this class is not key value coding-compliant for the key btnSize

책 예제를 따라하거나, 이미 만들어진 ViewController와 mainstory.swift를 합칠 경우,

내부 Widget에 정의한 action에 대해 연결이 깨진 경우가 발생한다.

확인하려면, Xcode에서 Triggered Segues에 링크가 깨져서 노란색으로 경고를 띄워주는 것을 발견할 수 있다. 이것을 다시 만들어서 정의해 주면 된다. 

Swift 오류 확인하는 화면

오류 내용은 아래와 같이 나타난다. 

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<UIViewController 0x103a17520> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key btnSize.'
*** First throw call stack:
(
	0   CoreFoundation                      0x00000001804b910c __exceptionPreprocess + 172
	1   libobjc.A.dylib                     0x0000000180092da8 objc_exception_throw + 72
	2   CoreFoundation                      0x00000001804b8ca0 -[NSException init] + 0
	3   Foundation                          0x0000000180e9da3c -[NSObject(NSKeyValueCoding) setValue:forKey:] + 268
	4   UIKitCore                           0x00000001853b7dc4 -[UIViewController setValue:forKey:] + 76
	5   UIKitCore                           0x00000001857435ec -[UIRuntimeOutletConnection connect] + 80
	6   CoreFoundation                      0x00000001804a5cf0 -[NSArray makeObjectsPerformSelector:] + 192
	7   UIKitCore                           0x0000000185736ee4 -[UINib instantiateWithOwner:options:] + 1420
    8   UIKitCore                           0x00000001853bf7a8 -[UIViewController loadView] + 392
	9   UIKitCore                           0x00000001853bfa64 -[UIViewController loadViewIfRequired] + 152
	10  UIKitCore                           0x00000001853c0000 -[UIViewController view] + 20
	11  UIKitCore                           0x00000001852f4628 -[UITabBarController transitionFromViewController:toViewController:transition:shouldSetSelected:] + 912
	12  UIKitCore                           0x00000001852eef70 -[UITabBarController _setSelectedViewController:performUpdates:] + 352
	13  UIKitCore                           0x00000001852eedd4 -[UITabBarController setSelectedViewController:] + 80
	14  UIKitCore                           0x00000001852f3a98 -[UITabBarController _setSelectedViewControllerAndNotify:] + 216
	15  UIKitCore                           0x00000001852f3964 -[UITabBarController _tabBarItemClicked:] + 160
	16  UIKitCore                           0x0000000185b36e3c -[UIApplication sendAction:to:from:forEvent:] + 96
	17  UIKitCore                           0x00000001850f969c -[UITabBar _sendAction:withEvent:] + 380
	18  UIKitCore                           0x0000000185b36e3c -[UIApplication sendAction:to:from:forEvent:] + 96
	19  UIKitCore                           0x000000018540c830 -[UIControl sendAction:to:forEvent:] + 108
	20  UIKitCore                           0x000000018540cb74 -[UIControl _sendActionsForEvents:withEvent:] + 268
	21  UIKitCore                           0x00000001850fbfb0 -[UITabBar _buttonUp:] + 96
	22  UIKitCore                           0x0000000185b36e3c -[UIApplication sendAction:to:from:forEvent:] + 96
	23  UIKitCore                           0x000000018540c830 -[UIControl sendAction:to:forEvent:] + 108
	24  UIKitCore                           0x000000018540cb74 -[UIControl _sendActionsForEvents:withEvent:] + 268
	25  UIKitCore                           0x000000018540b80c -[UIControl touchesEnded:withEvent:] + 392
	26  UIKitCore                           0x0000000185b6aa10 -[UIWindow _sendTouchesForEvent:] + 972
	27  UIKitCore                           0x0000000185b6be20 -[UIWindow sendEvent:] + 2840
	28  UIKitCore                           0x0000000185b4b80c -[UIApplication sendEvent:] + 376
	29  UIKitCore                           0x0000000185bd5c70 __dispatchPreprocessedEventFromEventQueue + 1156
	30  UIKitCore                           0x0000000185bd8c00 __processEventQueue + 5592
	31  UIKitCore                           0x0000000185bd0f10 updateCycleEntry + 156
	32  UIKitCore                           0x00000001850a5cec _UIUpdateSequenceRun + 76
	33  UIKitCore                           0x0000000185a60858 schedulerStepScheduledMainSection + 168
	34  UIKitCore                           0x0000000185a5fc90 runloopSourceCallback + 80
	35  CoreFoundation                      0x000000018041d294 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
	36  CoreFoundation                      0x000000018041d1dc __CFRunLoopDoSource0 + 172
	37  CoreFoundation                      0x000000018041c940 __CFRunLoopDoSources0 + 232
	38  CoreFoundation                      0x0000000180416e84 __CFRunLoopRun + 788
	39  CoreFoundation                      0x00000001804166f4 CFRunLoopRunSpecific + 552
	40  GraphicsServices                    0x00000001905e5b10 GSEventRunModal + 160
	41  UIKitCore                           0x0000000185b319dc -[UIApplication _run] + 796
	42  UIKitCore                           0x0000000185b35bd4 UIApplicationMain + 124
	43  UIKitCore                           0x0000000184f0a334 block_destroy_helper.22 + 9660
	44  Tab.debug.dylib                     0x0000000102f0103c $sSo21UIApplicationDelegateP5UIKitE4mainyyFZ + 120
	45  Tab.debug.dylib                     0x0000000102f00fb4 $s3Tab11AppDelegateC5$mainyyFZ + 44
	46  Tab.debug.dylib                     0x0000000102f010b8 __debug_main_executable_dylib_entry_point + 28
	47  dyld                                0x0000000102479410 start_sim + 20
	48  ???                                 0x00000001025ca154 0x0 + 4334592340
	49  ???                                 0x9874000000000000 0x0 + 10985405391063482368
)
libc++abi: terminating due to uncaught exception of type NSException
Exception	NSException *	
"[<UIViewController 0x103a17520> setValue:forUndefinedKey:]: 
this class is not key value coding-compliant for the key btnSize."	0x0000600000c9dfe0

 

Posted by 목표를 가지고 달린다
,

익명함수(클로져, Closure) 이해하기

  함수 비고
함수원본 func 함수명 ( 파라미터명 : 자료형) -> (반환자료형) {
             실행 구문
}
보통함수
1 { ( 파라미터명 : 자료형) -> (반환타입) in 실행 구문 } 익명함수
2 { (파라미터명) in 실행 구문 }
3 { 파라미터명 in 실행 구문 }

위의 방식을 이용한 아래의 예제를 보시면서 이해해보세요.

  함수 비고
함수원본 func completeWork(completed : Bool) -> () {
    print ("complete : \(completed)") 
보통함수
1 { (completed: Bool) -> () in print ("complete : \(completed)")  } 익명함수

2 { (completed: Bool)         in print ("complete : \(completed)")  }
3 { (completed)                  in print ("complete : \(completed)")  }
4 { completed                    in print ("complete : \(completed)")  }

위와 같이 실행구문은 변함이 없으나, 함수 호출을 정의하는 부분이 많이 축약(함수명, 파라미터의 자료형 등) 됩니다.

사실 클로져에 대한 활용은 많은 예시문을 별도로 보관하고 계시다가 발췌해서 이용하거나, 기존 것을 활용해서 수정해서 사용하시면서 익히시는 것이 많은 도움이 될 것입니다. 특히, 집합 관련하여 filter 등을 활용한 예제들은 소스코드를 많이 간결하게 만들어주기 때문에 꼭 한번 찾아 보세요.

Posted by 목표를 가지고 달린다
,

개발 과정에서 기본으로 제공되는 첫화면, Main.storyboard 파일명을 변경할 수 있습니다. 

Main.storyboard 파일명 변경시 오류 발생

스토리보드 파일 속성 창에서 "Is initial View Controller" 속성 값을 체크하면 될 것 같지만 사실 추가 변경작업을 해줘야 합니다.

파일명을 변경했는데, NSBundle 파일에서 Main이라는 storyboard파일이 없다는 오류가 발생합니다. 

NetflixClone[52739:34390677]
 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: 'Could not find a storyboard named 'Main' in bundle NSBundle 
</Users/abdurl/Library/Developer/CoreSimulator/Devices/
B70278F4-87BD-46B7-984B-B63EDE4CEBFC/data/Containers/Bundle/Application/
AE7F576F-36A8-446D-B54F-87C874367AD4/NeflixClone.app> (loaded)'
*** First throw call stack:

(해결방안) 프로젝트 설정 파일 수정(3군데)

1. Main storyboard file base name

2. Application Secene Manifest - Scene Configuration - item 0 - Storyboard Name

Application Secene Manifest - Scene Configuration - item 0 - Storyboard Name

3. Info.plist Values - UIKit Main Storyboard File Base Name

3군데를 모두 변경 후에도 인식하지 못한다면, XCODE를 재실행하여 캐시를 삭제하면 제대로 인식할 수 있습니다. 

 

Posted by 목표를 가지고 달린다
,

Swift 개발에서 RestApi를 호출하여 정보를 받아오는 경우가 많습니다. 그런 경우, 변수명 네이밍 규칙이 본인의 규칙과 다를 경우 CodingKeys를 이용하여 RestApi 명세의 항목과 개발하는 클래스(스트럭처)의 항목을 매핑하여 개발할 수 있습니다.

이때 주의 사항입니다.

struct MovieResult : Codable {
    let trackName : String?
    let previewUrl : String?
    let artworkUrl : String?
    let releaseDate : String?
    let shortDescription : String?
    let longDescription : String?
    
    enum CodingKeys : String, CodingKey {
        case trackName
        case previewUrl
        case artworkUrl = "artworkUrl100"
        case releaseDate
        case shortDescription
        case longDescription
    }
}
  • response값에 특정항목이 없을 경우를 대비하여 String? , Int? 형태로 nil 을 대비한다
  • response항목의 이름을 다르게 사용할 경우, enum CodingKeys : String, CodingKey{ ... } 를 정의한다.
  • enum CodingKeys에 모든 항목을 정의해야 한다. 누락시 오류 발생. 이름을 다르게 사용하고 싶은 항목만 정의(예시:artworkUrl)

만약, enum CodingKeys에 항목을 누락하거나 오타를 치게 되면, 아래와 같이 오류가 발생한다. 

Type *** does not conform to protocol 'Decodable'
Type *** does not conform to protocol 'Encodable'

type does not conform to protocol Decodable, Encodable

 

Posted by 목표를 가지고 달린다
,

11장. 내비게이션 컨트롤러 이용해 화면 전환하기

12장.  테이블 뷰 컨트롤러 이용해 할일 목록 만들기

#  내비게이션 컨트롤러 이용해 화면 전환하기

가. "Editor-Embed in-Navigation Controller"를 선택합니다.

viewController화면을 클릭한 후, navigation Controller 버튼을 클릭

나. library 팔레트에서 "View Controller"를 끌어다가 메인화면 컨트롤러의 오른쪽 빈 공간에 놓습니다.

View 추가

다. library 팔레트에서 "Bar Button Item"을 찾아 네비게이션 바의 오른쪽에 끌어다 놓습니다. 그리고 Arributes inspector의 system item 에 값을 Edit 등으로 변경합니다.

라. EDIT 버튼을 마우스 오른쪽 버튼으로 클릭한 채 오른쪽의 뷰 컨트롤러에 갖다 놓습니다. 그러면 검은색 창에 Action Segue를 Show로 선택합니다. 그러면 "메인화면"에서 서브화면으로 갔다가 돌아오는 형태를 취하게 됩니다. 그러면 별도의 코딩이 없어도 화면 전환이 가능합니다. 

마. 새로운 화면은 뷰 컨트롤러 클래스 파일이 없어, 메뉴에서 File-New-File.. 에서 Cocoa Touch Class를 선택합니다. SubClass는 UIViewController로 하고 클래스명은 ***ViewController로 저장합니다.

view controller 파일 생성

바. Main.storyboard 파일에서 새로 추가한 뷰 컨트롤러(화면)을 선택한후, Identity inspector를 클릭한 후, Class에 새로 만든 ***ViewController 를 선택하여 연결시킵니다. 

class ViewController: UIViewController {

    var images = ["caterpillar.jpg", "child.jpg", "dahlias.jpg", "strawberry.jpg", "woman.jpg"]
    
    @IBOutlet var imgView: UIImageView!
    @IBOutlet var pageControl: UIPageControl!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        print (images.count)
        pageControl.numberOfPages = images.count
        pageControl.currentPage = 0
        
        pageControl.pageIndicatorTintColor = UIColor.green
        pageControl.currentPageIndicatorTintColor = UIColor.red
        imgView.image = UIImage(named: images[0])
    }

    @IBAction func pageChange(_ sender: UIPageControl) {
        print("current page : ")
        print (pageControl.currentPage)
        imgView.image = UIImage(named: images[pageControl.currentPage])
    }
    
}

사. prepare()함수를 재정의하여 값을 전달하고 전달자가 누구인지는 segue.identifier값을 통해 알 수 있습니다.

class ViewController: UIViewController, EditDelegate {
     

    let imgOn = UIImage(named: "lamp_on.png")
    let imgOff = UIImage(named: "lamp_off.png")
    
    var isOn = true
    @IBOutlet var txMessage: UITextField!
    
    @IBOutlet var imgView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        imgView.image = imgOn
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender : Any?) {
        let editViewController = segue.destination as! EditViewController
        if segue.identifier == "editButton" {
            //버튼을 클릭한 경우
            editViewController.textWayValue = "segue : use button"
        } else if segue.identifier == "editBarButton" {
            //바 버튼을 클릭한 경우
            editViewController.textWayValue = "segue : use Bar button"
        }
        editViewController.isOn = isOn
        editViewController.textMessage = txMessage.text!
        editViewController.delegate = self
    }
    
    func didMessageEditDone(_ controller: EditViewController, message: String ) {
        txMessage.text = message
         
    }
    func didImageOnOffDone(_ controller: EditViewController, isOn: Bool) {
        self.isOn = isOn
        if isOn {
            imgView.image = imgOn
        } else {
            imgView.image = imgOff
        }
    }


}

아.데이터를 전달할 경우, delegate역할의 프로토콜을 작성 및 프로토콜내 함수 정의

import UIKit

protocol EditDelegate {
    func didMessageEditDone(_ controller : EditViewController, message : String )
    func didImageOnOffDone(_ controller : EditViewController, isOn : Bool)
}

class EditViewController: UIViewController {

자. didMessageEditDone함수에서 메시지 전달 : txMessage.text = message

차. EditViewController에서 delegate 변수 생성 : var delegate : EditDelegate?

카. 수정화면의 btnDone()함수 를 통해 메인화면으로 값 전달하는 부분 생성 : delegate?.didMessageEditDone(self, message : txMessage.text!)

타. 메인스토리의 ViewController에서 prepare()문에 한줄 추가 : editViewController.delegate = self

//
//  ViewController.swift
//  Navigation
//
//  Created by abdurl on 2023/09/20.
//

import UIKit

class ViewController: UIViewController, EditDelegate {
     

    let imgOn = UIImage(named: "lamp_on.png")
    let imgOff = UIImage(named: "lamp_off.png")
    
    var isOn = true
    @IBOutlet var txMessage: UITextField!
    
    @IBOutlet var imgView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        imgView.image = imgOn
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender : Any?) {
        let editViewController = segue.destination as! EditViewController
        if segue.identifier == "editButton" {
            //버튼을 클릭한 경우
            editViewController.textWayValue = "segue : use button"
        } else if segue.identifier == "editBarButton" {
            //바 버튼을 클릭한 경우
            editViewController.textWayValue = "segue : use Bar button"
        }
        editViewController.isOn = isOn
        editViewController.textMessage = txMessage.text!
        editViewController.delegate = self
    }
    
    func didMessageEditDone(_ controller: EditViewController, message: String ) {
        txMessage.text = message
         
    }
    func didImageOnOffDone(_ controller: EditViewController, isOn: Bool) {
        self.isOn = isOn
        if isOn {
            imgView.image = imgOn
        } else {
            imgView.image = imgOff
        }
    }


}
import UIKit

protocol EditDelegate {
    func didMessageEditDone(_ controller : EditViewController, message : String )
    func didImageOnOffDone(_ controller : EditViewController, isOn : Bool)
}

class EditViewController: UIViewController {
    
    
    var textWayValue : String = ""
    var textMessage : String = ""
    var delegate : EditDelegate?
    var isOn = true

    @IBOutlet var txMessage: UITextField!
   
    @IBOutlet var lblWay: UILabel!
    
    @IBOutlet var swIsOn: UISwitch!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        lblWay.text = textWayValue
        txMessage.text = textMessage
        swIsOn.isOn = isOn

        // Do any additional setup after loading the view.
    }
    

    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destination.
        // Pass the selected object to the new view controller.
    }
    */

    @IBAction func btnDone(_ sender: UIButton) {
        
        if delegate != nil {
            delegate?.didMessageEditDone(self, message: txMessage.text!)
            delegate?.didImageOnOffDone(self, isOn: isOn)
        }
        //navigationController?.popViewController(animated: true)
        _ = navigationController?.popViewController(animated: true)
    }
    
    @IBAction func swImageOnOff(_ sender: UISwitch) {
        
        if sender.isOn {
            isOn = true
        } else {
            isOn = false
        }
    }
}

 

#  테이블 뷰 컨트롤러 이용해 할 일 목록 만들기

가. 아이폰 모양의 스토리보드의 상단을 드래그 한 후, "delete" 버튼으로 화면에 내용 삭제 : 빈 화면 만들기

나. 프로젝트 내의 ViewControllers.swift파일 삭제

다. Library 팔레트에서 Table View Controller를 선택해서 스토리보드에 올려 놓습니다.

라. 메뉴에서 Editor-Embed in-Navigation Controller를 선택하여 추가(Attributes inspector에서 "is Initial View Controller" 항목에 체크)

마. Library팔레트에서 View Controller를 2개를 선택해서 화면에 추가(화면 2개 추가)

바. Library팔레트에서 "Bar Button Item(바 버튼 아이템)"을 찾아 테이블 뷰 컨트롤러의 오른쪽 윗부분에 배치합니다. "Attributes Inspector"을 클릭한 후, System Item값을 Add로 수정합니다. 

사. 바 버튼 아이템을 마우스 오른쪽 버튼으로 클릭하여 새로 추가한 뷰 컨트롤러(마.에서 추가한 화면)로 드래그 합니다. 그리고 뷰 컨트롤러가 전체적으로 파랗게 되면 마우스 버튼을 놓습니다. 그리고 Action Segue 창에서 Show를 선택합니다. 

.. 이후 부분에 대해서는 내용이 많고, 책이 정리가 잘 되어 있기에 실제 책을 보거나 영상 강의를 보는 것을 추천합니다. 저도 이 책은 여기까지 마무리 하고 최근 구매한 인프런의 강좌를 듣고자 합니다. 

****** 추가 ********

segue 설정으로 show를 선택한 후, 이전화면으로 돌아갈 경우, show는 기존 화면 위에 올려둔 것이라, 현재 화면을 pop시키면 되기에 아래와 같이 어떤 ViewController를 사용하는지 몰라도 동일하게 작동할 수 있다.

@IBAction func btnDone(_ sender: UIButton) {
	_ = navigationController?.popViewController(animated: true)
}

***** 목록 삭제 하기 *****

item을 삭제처리하는 부분을 추가하고, 문구를 변경(delete -> 삭제) 로 변경하면 끝.. 그외는 모든 것은 기본 제공.

// Override to support editing the table view.
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            // Delete the row from the data source
            items.remove(at: (indexPath as NSIndexPath).row)
            itemsImageFile.remove(at: (indexPath as NSIndexPath).row)
            tableView.deleteRows(at: [indexPath], with: .fade)
        } else if editingStyle == .insert {
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
        }    
    }
    
    // 문구를 Delete에서 "삭제" 로 변경하는 방법
    override func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? {
        return "삭제"
    }

******* 목록 삭제하기2 *******

override func viewDidLoad() {
        super.viewDidLoad()
        
        // 아래 코드는 주석처리 되어 있었기에, 주석해제만 하면 됨... 즉 // 만 삭제하면 됨
        self.navigationItem.rightBarButtonItem = self.editButtonItem
        
        // 아래와 같이 왼쪽에 Edit 버튼을 배치할 수도 있습니다.
        //self.navigationItem.leftBarButtonItem = self.editButtonItem
}

***** 목록 편집하기 (목록 순서바꾸기)  *****

아래의 함수는 원래 주석되어 제공되는데, 주석을 풀고 실행을 하면... "Edit" 편집버튼을 누르면, 오른쪽에 3줄짜리 버거 버튼이 항목마다 오른쪽에 생긴다. 그리고 순서를 저장해야 한다면, 아래와 같이 코드를 추가해야 한다.

 

override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
        
        let itemToMove = items[ (fromIndexPath as NSIndexPath).row]
        let itemImageToMove = itemsImageFile[ (fromIndexPath as NSIndexPath).row]
        
        items.remove(at: (fromIndexPath as NSIndexPath).row)
        itemsImageFile.remove(at: (fromIndexPath as NSIndexPath).row)
        
        items.insert(itemToMove, at: (to as NSIndexPath).row)
        itemsImageFile.insert(itemImageToMove, at: (to as NSIndexPath).row)
         
}

**** 목록 갱신(추가 항목이 보이지 않는다면..) *****

 뷰가 처음 보일때 호출 되는 함수의 순서는 ViewDidLoad -> ViewWillAppear -> ViewDidAppear 순입니다. 하지만, 뷰가 전환되어 나타날 때는 ViewWillAppear -> ViewDidAppear 만 호출됩니다. !  이것으로 최초 화면 로딩시에만 호출할 내용은 ViewDidLoad에 넣으면 됩니다. 

class TableViewController: UITableViewController {

    @IBOutlet var tvListView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.leftBarButtonItem = self.editButtonItem
    }
    
    // List를 reload 한다.
    override func viewWillAppear(_ animated: Bool) {
        tvListView.reloadData() 
    }
    
    ........
}

class AddViewController: UIViewController {

    @IBOutlet weak var tfAddItem: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func btnAddItem(_ sender: UIButton) {
        items.append(tfAddItem.text!)
        itemsImageFile.append("pencil.jpeg")
        //tfAddItem.text = ""
        _ = navigationController?.popViewController(animated: true)
    }
}

****** detail View 만들기(상세 페이지 만들기) *****

DetailView 클래스에는 전달값을 받아 처리하는 부분을 정의(단순 문자열을 받거나, 문자열을 키로 해서 파일이나 DB를 읽어 Set)하는 것과 TableListView에서 값을 전달하는 함수를 정의하면 된다.

class TableViewController: UITableViewController {

  .......
  
   override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destination.
        // Pass the selected object to the new view controller.
        if segue.identifier == "sgDetail" {
            let cell = sender as! UITableViewCell
            let IndexPath = self.tvListView.indexPath(for: cell)
            let detailView = segue.destination as! DetailViewController
            detailView.receiveItem(items[(IndexPath! as NSIndexPath).row])   
        }
    }
}

class DetailViewController: UIViewController {

      ......

	func receiveItem (_ item : String) {
        receiveItem = item
    }
}
Posted by 목표를 가지고 달린다
,

9장.  페이지 이동하기 - 페이지 컨트롤

10장.  탭 바 컨트롤러 이용해 여러 개의 뷰 넣기

# 페이지 이동하기 - 페이지 컨트롤

library 팔레트에서 'page controll'를 찾아 화면의 image View객체 아래에 놓습니다. 

페이지 컨트롤러

pageChange()를 작성해서 pageControl의 currentPage속성값을 이용해서 화면을 변경할 수 있습니다. 

class ViewController: UIViewController {

    var images = ["caterpillar.jpg", "child.jpg", "dahlias.jpg", "strawberry.jpg", "woman.jpg"]
    
    @IBOutlet var imgView: UIImageView!
    @IBOutlet var pageControl: UIPageControl!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        print (images.count)
        pageControl.numberOfPages = images.count
        pageControl.currentPage = 0
        
        pageControl.pageIndicatorTintColor = UIColor.green
        pageControl.currentPageIndicatorTintColor = UIColor.red
        imgView.image = UIImage(named: images[0])
    }

    @IBAction func pageChange(_ sender: UIPageControl) {
        print("current page : ")
        print (pageControl.currentPage)
        imgView.image = UIImage(named: images[pageControl.currentPage])
    }
    
}

* 삼원색인 RGB(red green blue)와 투명도 Alpha 값을 사용하는 방법은 아래의 UIColor클래스을 이용하는 것으로, rgb는 0~1사이의 실수값으로 사용되는 색상은 0~255사이의 값으로 표현됩니다. 투명도(alpha)는 0~1사이의 실수값을 가집니다.(0:투명, 1:불투명)

init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)

// example 
let red = UIColor(red:1, green:0, blue:0, alpha:1)
let myColor = UIColor(red:1, green : 165/255, blue:0, alpah:1)

# 탭 바 컨트롤러 이용해 여러 개의 뷰 넣기

가. 탭바 컨트롤러 추가 : 스토리보드(Main.swift)의 아이폰 화면 전체를 드래그한 후 메뉴에서 "Editor-Embed in-Tab Bar Controller"를 선택합니다. 

아이폰 화면을 전체 선택한 후, Editor-Embed in-Tab Bar Controller 선택

나. 다른 프로젝트 등에 있던 ViewController.swift파일을 복사해서 이름을 MapViewController.swift등 변경 후, 현재 프로젝트내 ViewController 파일과 동일한 위치에 배치합니다. (파일명을 바꾸면서, 파일안에 클래스명도 MapViewController로 동일하게 변경)

다. 다른 프로젝트의 스토리보드의 화면을 전체 선택해서 복사한 이후, 현재 프로젝트의 스토리보드 상의 빈곳을 클릭한 후 붙여넣기(command+V)를 합니다. 이따, Identity inspector를 클릭해서 Class를 ViewController가 아닌 MapViewController로 변경합니다. 

라. "Tab Bar Controller" 라고 적힌 뷰 컨트롤러를 마우스 오른쪽 버튼을 클릭한 후, 다.에서 복사한 뷰 화면 안의 빈공간에 드래그합니다. 그러면 연결선이 나타나며 뷰 컨트롤러가 전체적으로 파랗게 됩니다. 이 때 손을 놓으면 됩니다. 

탭 바에서 새로운 ViewController 연결
연결은 view controllers로 설정

스토리보드(화면) 당 viewController.swift 한개로 1:1 매칭임을 기억하고, 화면 전환은 tabBarController?.selectedIndex = 1 이런식으로 변경할 수 있다. 또한 다른 프로젝트의 화면과 viewController를 그대로(이름만 변경해서) 가져오기 때문에 재활용성이 높습니다.

class ViewController: UIViewController {
 
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    
    @IBAction func btnMoveImageView(_ sender: UIButton) {
        tabBarController?.selectedIndex = 1
    }
    
    
    @IBAction func btnMoveDatePickerView(_ sender: UIButton) {
        tabBarController?.selectedIndex = 2
    }
    
}

요약
- 페이지컨트롤러를 이용할 수 있습니다. 
- 탭바를 이용하면기존 화면을 최대한 재활용할 수 있습니다. 화면 1개당 1개의 viewController.swift가 필요합니다.

Posted by 목표를 가지고 달린다
,

7장. 웹 뷰로 간단한 웹 브라우저 만들기

8장. 맵 뷰로 지도 나타내기

 

# 웹뷰로 간단한 웹 브라우저 만들기

가. 웹뷰 제작 기초(권한 설정 등)

url을 String으로 받아  URLRequest로 받은 후, Web View 객체에 로드하면 됩니다. 만약 시뮬레이터에서 작동하지 않는다면, 프로젝트에서 Info.plist 파일을 열어 인터넷 관련 권한을 추가 설명해야 합니다. App Transport Security Settings에 + 를 누르고, Allow Arbitrary Loads를 선택하고 Value를 No 에서 Yes로 변경합니다. 그리고 시뮬레이터를 재시작합니다. 

func loadWebPage(_ url:String) {
	let myUrl = URL(string: url)
    let myRequest = URLRequest(url : myUrl!)
    myWebView.load(myrequest)
}

나. 액티비티 인디케이터로 로딩보이기

로딩을 기다릴 때, 화면 가운데서 돌아가는 원 모양의 점선이 바로 '액티비티 인디케이터 뷰'입니다. library 팔레트에서 Activity Indicator View를 선택한 후, WebKit View 위에 올려둔 후, Hide When Stopped로 동작을 멈추면 보이지 않게 설정합니다. 그 이후 코딩으로 webView를 설정하여 애니메이션을 작동.중지시키고, 숨기는 등의 행동의 재정의합니다. 

class ViewController: UIViewController, WKNavigationDelegate {

    @IBOutlet var txtUrl: UITextField!
    
    @IBOutlet var myWebView: WKWebView!
    
    @IBOutlet var myActivityIndicator: UIActivityIndicatorView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        myWebView.navigationDelegate = self
        loadWebPage("http://2sam.net")
    }
     
    func webView(_ webView: WKWebView, didCommit navigation : WKNavigation!) {
        myActivityIndicator.startAnimating()
        myActivityIndicator.isHidden = false
    }
    func webView(_ webView: WKWebView, didFinish  navigation : WKNavigation!) {
        myActivityIndicator.stopAnimating()
        myActivityIndicator.isHidden = true
    }
    func webView(_ webView: WKWebView, didFail navigation : WKNavigation!) {
        myActivityIndicator.stopAnimating()
        myActivityIndicator.isHidden = true
    }
    
    func loadWebPage(_ url:String) {
        let myUrl = URL(string: url)
        let myRequest = URLRequest(url:myUrl!)
        myWebView.load(myRequest)
        
    }

    @IBAction func btnGotoUrl(_ sender: UIButton) {
        let myUrl = checkUrl(txtUrl.text!)
        loadWebPage(myUrl)
    }
     
    @IBAction func btnGoSite1(_ sender: UIButton) {
        loadWebPage("http://fallinmac.tistory.com")
    }
    
    @IBAction func btnGoSite2(_ sender: UIButton) {
        loadWebPage("http://blog.2sam.net")
    }
    
    @IBAction func btnLoadHtmlString(_ sender: UIButton) {
        let htmlString = """
            <H1> HTML String</H1>
            <P>String 변수를 이용한 웹 페이지</P>
            <p><a href= \"http://2sam.net\">2sam</a>으로 이동</p>"
            """
        myWebView.loadHTMLString(htmlString, baseURL: nil)
    }
    
    @IBAction func btnLoadHtmlFile(_ sender: UIButton) {
        let filePath = Bundle.main.path(forResource: "htmlView", ofType: "html")
        let myUrl = URL(fileURLWithPath:filePath!)
        let myRequest = URLRequest(url:myUrl)
        myWebView.load(myRequest)
    }
    
    @IBAction func btnStop(_ sender: UIBarButtonItem) {
        myWebView.stopLoading()
    }
    
    @IBAction func btnReload(_ sender: UIBarButtonItem) {
        myWebView.reload()
    }
    
    @IBAction func btnGoBack(_ sender: UIBarButtonItem) {
        myWebView.goBack()
    }
    
    @IBAction func btnGoForward(_ sender: UIBarButtonItem) {
        myWebView.goForward()
    }
    
    func checkUrl(_ url: String) -> String {
        var strUrl = url
        let flag = (strUrl.hasPrefix("http://") || strUrl.hasPrefix("https://") )
        if !flag {
            strUrl = "http://" + strUrl
            print(strUrl)
        }
        return strUrl
    }
    
}

Refresh, Stop, Forward, Backward 등의 아이콘은 속성창의 System Item에서 제공하고 있어 이용하면 됩니다. 

제공하는 아이콘(Refresh)를 이용하는 모습

다. 앱 로딩시, 웹페이지 나타나도록 하기(로딩)

 "Info.plist" 파일을 수정해야 함. 목록에서 [App Transport Security Settings]을 선택한 후, "+" 를 클릭하여, [Allow Arbitrary Load]을 선택하고, 오른쪽 Value의 값을 "NO"에서 "YES"로 변경하면, 실행시 지정한 웹페이지가 보임.

라. 액티비티 인디케이터로 로딩보이기

Delegate를 추가하고, func 3개를 추가(로딩 중, 로딩 완료, 로딩 실패)

import UIKit
import WebKit

class ViewController :UIViewController, WKNavigationDelegate {

    ..............

    // 로딩 중일때,
    func webView(_ webView: WKWebView, didCommit navigation : WKNavigation!) {
        myActivityIndicator.startAnimating()
        myActivityIndicator.isHidden = false
    }
    // 로딩 완료시
    func webView(_ webView: WKWebView, didFinish navigation:WKNavigation!) {
    	myActivityIndicator.stopAnimating()
        myAcitvityIndicator.isHidden = true
    }
    //로딩 실패시
    func webView(_ webView: WKWebView, didFail navigation : WKNavigation!, withError error : Error) {
    	myActivityIndicator.stopAnimating()
        myActivityIndicator.isHidden = true
    }
    
}

 

# 맵 뷰로 지도 나타내기

- 맵뷰로 별도로 세그먼트 컨트롤이 있는데, 기능상 버튼과 동일한데 실제 어떤 것이 선택되었는지 알 수 있어 편리함

세그먼트 컨트롤(segment control) 화면

맵뷰를 실행했는데 아래와 같은 오류가 발생했다면, 프로젝트의 Info.plist파일을 열어 Information Property List 위로 가져가 + 클릭하여 "Privacy-Location When In Use Usage Description"을 선택하고 value를 더블클릭하여 "App needs location servers for stuff"로 수정하면 됩니다. 

This app has attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an “NSLocationWhenInUseUsageDescription” key with a string value explaining to the user how the app uses this data
[VKDefault] Missing MeshRenderables for ground mesh layer for (4/4) of ground tiles. Tile debug info: (Key: 55.24.6.255 t:34 kt:0, Has mesh errors: 0, MeshInstance count: 1, PendingMaterial count: 1, Invisible MeshInstances count: 0 | Key: 54.25.6.255 t:34 kt:0, Has mesh errors: 0, MeshInstance count: 1, PendingMaterial count: 1, Invisible MeshInstances count: 0 | Key: 55.25.6.255 t:34 kt:0, Has mesh errors: 0, MeshInstance count: 1, PendingMaterial count: 1, Invisible MeshInstances count: 0 | Key: 54.24.6.255 t:34 kt:0, Has mesh errors: 0, MeshInstance count: 1, PendingMaterial count: 1, Invisible MeshInstances count: 0)
[Font] Failed to parse font key token: hiraginosans-w6
[Font] Failed to parse font key token: hiraginosans-w6
class ViewController: UIViewController, CLLocationManagerDelegate {
    
    let locationManager = CLLocationManager()

    @IBOutlet var myMap: MKMapView!
    
    @IBOutlet var lblLocationInfo1: UILabel!
    
    @IBOutlet var lblLocationInfo2: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        lblLocationInfo1.text = ""
        lblLocationInfo2.text = ""
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
        myMap.showsUserLocation = true
    }
 
    func goLocation(latitudeValue : CLLocationDegrees, longitudeValue : CLLocationDegrees, delta span : Double ) -> CLLocationCoordinate2D{
        let pLocation = CLLocationCoordinate2DMake(latitudeValue, longitudeValue)
        let spanValue = MKCoordinateSpan(latitudeDelta: span, longitudeDelta: span)
        let pRegion = MKCoordinateRegion(center:pLocation, span:spanValue)
        myMap.setRegion(pRegion, animated: true)
        return pLocation
        
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        let pLocation = locations.last
        _ = goLocation(latitudeValue: (pLocation?.coordinate.latitude)!, longitudeValue: (pLocation?.coordinate.longitude)!, delta: 0.01)
        CLGeocoder().reverseGeocodeLocation(pLocation!, completionHandler: {
            (placemarks, error) -> Void in
            let pm = placemarks!.first
            let country = pm!.country
            var address:String = country!
            if pm!.locality != nil {
                address += " "
                address += pm!.locality!
            }
            if pm!.thoroughfare != nil {
                address += " "
                address += pm!.thoroughfare!
            }
            
            self.lblLocationInfo1.text = "현재 위치"
            self.lblLocationInfo2.text = address
        })
        
        locationManager.stopUpdatingLocation()
    }
    
    // delta는 축약정도로 0.01이면, 지도를 100배로 확대해서 보여준다. 
    // 위도와 경도로 원하는 핀 설치.
    func setAnnotation(latitudeValue : CLLocationDegrees,
                       longitudeValue : CLLocationDegrees, delta span : Double, title strTitle:String, subtitle strSubtitle:String) {
        let annotation = MKPointAnnotation()
        annotation.coordinate = goLocation(latitudeValue:latitudeValue, longitudeValue: longitudeValue, delta: span)
        annotation.title = strTitle
        annotation.subtitle = strSubtitle
        myMap.addAnnotation(annotation)
    }
    
  
    @IBAction func sgChangeLocation(_ sender: UISegmentedControl) {
        if (sender.selectedSegmentIndex == 0 ) {
            self.lblLocationInfo1.text = " "
            self.lblLocationInfo2.text = " "
            locationManager.startUpdatingLocation()
            
        } else if sender.selectedSegmentIndex == 1 {
            setAnnotation(latitudeValue: 37.751853, longitudeValue: 128.87605740000004, delta: 1, title: "한국폴리텍대학 강릉캠퍼스", subtitle: "강원도 강릉시 남산초교길 121")
            self.lblLocationInfo1.text = "보고 계신 위치"
            self.lblLocationInfo2.text = "한국폴리텍대학 강릉캠퍼스"
        } else {
            setAnnotation(latitudeValue: 37.556876, longitudeValue: 126.914066, delta: 0.1, title: "이지스빌딩", subtitle: "서울시 마포구 잔다리로 109 이지스 빌딩")
            self.lblLocationInfo1.text = "보고 계신 위치"
            self.lblLocationInfo2.text = "이지스퍼블리싱 출판"
            
        }
    }
    
}

요약
- WebView로 기본기능이 있는 브라우저를 만들수 있다. Info.plist파일에서 권한 설정을 하고, 아이콘들은 모두 설정에서 필요에 맞는 것을 찾아 바꿔준다.
- MapView는 지도 권한을 Info.plist파일에 부여하고, 즐겨찾기/특정지역도착시 알림음 등을 개발할 수 있다. 

Posted by 목표를 가지고 달린다
,

5장. Picker View를 사용해 원하는 항목 선택하기

6장. Alert 사용해 경고 메시지 표시하기

# Picker View : 여러 항목 중 하나를 선택하는 화면

 

피커뷰 사용 예시

피커 뷰가 상호작용하려면 피커 뷰에 대한 델리게이트 메소드를 사용해야 합니다. 특정 객체와 상호 작용할 때 메시지를 넘기면 그 메시지에 대한 책임은 델리게이트로 위임됩니다. 즉, 사용자가 객체를 터치했을 때 해야할 일을 델리게이트 메소드에 구현하고 해당 객체가 터치되었을 때 델리게리트가 호출되어 위임 받은 일을 하게 되는 것입니다. 

가. 피커 뷰의 델리게이트 사용을 설정하기 위하여 마우스 오른쪽 버튼으로 피커 뷰를 선택한 후, 위쪽의 뷰 컨트롤러 아이콘 위로 끌어다 놓습니다. 그리고 선택화면이 나오면 'delegate'를 선택합니다. 

맥북 마우스패드의 오른쪽에서 피커뷰를 클릭(누른 상태)하고 위쪽의 뷰 컨트롤러 아이콘 위로 끌어다 놓습니다.
delegate를 선택합니다.

나. 추가로 상속받을 클래스를 선언하고, 필요 pickerView들을 재정의합니다. 

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
	let MAX_ARRAY_NUM = 10
    let PICKER_VIEW_COLUMN = 1 // 한번에 보여질 항목 갯수
    let imageFileName= [ "1.jpg", "2.jpg", ..., "10.jpg"]
    
    var imageArray = [UIImage?]()
    
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
    	return PICKER_VIEW_COLUMN
    }
    func pickerView(_ pickerView : UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    	return imageFileName.count
    }
    func pickerView(_ pickerView : UIPickerView, titleForRow row : Int, forComponent component:Int) -> String? {
    	return imageFileName[row]
    }
    /* 선택되었을 때 행동 정의 */
    func pickerView(_ pickerView: UIPickerView, didSelectRow row : Int,
         inComponent component : Int) {
         lblImageFileName.text = imageFileName[row]
         imageView.image = imageArray[row]
    }
    
    override func viewDidLoad() {
    	super.viewDidLoad()
        
        for i in 0 ..< MAX_ARRAY_NUM {
        	let image = UIImage(named: imageFileName[i])
            imageArray.append(image)
        }
        lblImageFileName.text = imageFileName[0]
        imageView.image = imageArray[[0]
    }    
}

다. 피커뷰 룰렛에 파일명 대신 이미지를 출력한다면, 다른 pickerView를 정의해야 합니다.

// 중략
	let PICKER_VIEW_HEIGHT : CGFloat = 80

	//타이틀을 표기하는 pickerView를 우선 주석 처리
    //func pickerView(_ pickerView : UIPickerView, titleForRow row : Int, forComponent component:Int) -> String? {
    //	return imageFileName[row]
    //}
    
	func pickerView(_ pickerView: UIPicker/view, viewForRow row: int, forComponent component : int, reusing view: UIView?) -> UIView {
    	let imageView = UIImageView(image:imageArray[row])
        imageView.frame = CGRect(x:0, y:0, width : 100, height : 150)
        return imageView
    }
    
    // 표시할 이미지의 높이 설정 
    func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
    	return PICKER_VIEW_HEIGHT
    }    

// 생략

# Alert를 사용해서 경고 메시지 표시하기

Alert, Action을 정의한 후 Alert에 Action을 하단에 붙여서 present한다고 생각하면 됩니다.

let lampOnAlert = UIAlertController(title:"경고", message: "현재 on 상태입니다.", preferredStyle : UIAlertController.Style.alert)

let onAction = UIAlertAction(title:"네, 알겠습니다.", style: UIAlertAction.Style.default, handler:nil)

lampOnAlert.addAction(onAction)

present(lampOnAlert, animated:true, completion:nil)

위부분이 Alert문구, 아랫부분이 Action문구

네, 아니오에 대항 행동(Action) 정의하기

네, 아니오에 대한 액션을 정의한후, Alert에 추가합니다. 액션에 대한 정의를 클로저(Closer)로 정의해서 handler에 대입합니다.  

let lampOffAlert = UIAlertController(title:"램프 끄기", message: "램프를 끄시겠습니까?", preferredStyle : UIAlertController.Style.alert)

let offAction = UIAlertAction(title:"네", style: UIAlertAction.Style.default, handler: {
                ACTION in self.imgLamp.image = self.imgOff
                self.isLampOn = false
            })
        
let cancelAction = UIAlertAction(title:"아니오", style: UIAlertAction.Style.default, handler:nil)

lampOffAlert.addAction(offAction)
lampOffAlert.addAction(cancelAction)

present(lampOffAlert, animated: true, completion: nil)

요약 : 
- 피커 뷰는 delegate를 설정한 후, pickerView라고 타이핑하면 나오는 함수들 중에서 필요한 것을 재정의하면 됩니다. 
- Alert창은 alert, action을 정의해서 합친 이후 필요시 action에 따른 동작(이벤트)을 handler에 연결시켜주면 됩니다. 

Posted by 목표를 가지고 달린다
,