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)를 이용하는 모습

# 맵 뷰로 지도 나타내기

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

세그먼트 컨트롤(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 목표를 가지고 달린다
,

3장. 원하는 이미지 화면에 출력(사이즈 변경)

4장. 데이트 피커 사용해 날짜 선택하기(날짜 포맷, 타이머 개발)

Do it 스위프트

# 원하는 이미지 화면에 출력(사이즈 변경)

이미지를 선언하고, 변수에 할당하는 것은 다른 언어와 비슷합니다. 다만, 이미지 사이즈를 변경할 때 변수타입이 CGFloat타입임을 알아야 하며, 크기조절할 때 ImageView객체.frame.size의 값을 CGSize(width:,height:)로 재설정한다는 것을 기억하면 됩니다. 

var isZoom = false
var imgOn : UIImage?
var imgOff : UIImage?

     
@IBOutlet var imgView: UIImageView!
    
@IBOutlet var btnResize: UIButton!

override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        imgOn = UIImage(named: "lamp_on.png")
        imgOff = UIImage(named: "lamp_off.png")
        
        imgView.image = imgOn
        
}

@IBAction func btnResizeImage(_ sender: UIButton) {
        let scale : CGFloat = 2.0
        var newWidth:CGFloat , newHeight:CGFloat
        
        if (isZoom) {
            newWidth = imgView.frame.width / scale
            newHeight = imgView.frame.height / scale
            
            btnResize.setTitle("축소", for: .normal)
        } else {
            newWidth = imgView.frame.width * scale
            newHeight = imgView.frame.height * scale
            
            btnResize.setTitle("확대", for: .normal)
        }
        
        imgView.frame.size = CGSize(width: newWidth, height: newHeight)
        isZoom = !isZoom
}

데이트 피커 사용해 날짜 선택하기(날짜 포맷, 타이머 개발)

라이브러리에서 데이트피커(Date Picker)를 선택해서 메인스토리보드에 배치하고, Attributes inspector 를 클릭한 후, Style을 wheel로 변경하고, mode를 원하는 값을 설정합니다. 기본 설정값은 Date and Time입니다. 표기를 한글로 바꾸려면 Attributes inspector 에서 Locale 값을 Korean 으로 변경합니다. 

// fomatter를 이용해서 날짜와 시간을 표시하는 방법
@IBAction func changeDatePicker(_ sender: UIDatePicker) {
    let datePickerView = sender
        
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd HH:mm EEEE"
    lblPickerTime.text = "선택시간 : " + formatter.string(from:datePickerView.date)
}
필드 심벌 결과 의미
년도(Year) yy 23 두 자리로 연도 표시
yyyy 2023 네 자리로 연도 표시
월(Month) M 9 한글자로 월 표시
MM 09 두 자리로 월 표시
MMM Sep 영문 3자리로 월 표시
MMMM September 영문 풀단어로 월 표시
주(Week) w 6 1~52까지 연간 주 순서(week of year) 표시
ww 06 01~52까지 2자리로 연간 주 순서(week of year) 표시
W 4 1~6까지 월간 주 순서(week of month) 표시
일(Day) d 8 1~31 까지 일을 표시
dd 08 01~32 까지 2자리로 일을 표시
D 35 1~366까지 연간 일 순서(day of year)를 표시
DD 35 01~366까지 연간 일 순서(day of year)를 표시
DDD 035 001~366까지 연간 일 순서(day of year)를 표시
요일(weekday) E,EE,EEE Mon Sunday~Saturday까지 3글자로 요일 표시
EEEE Monday Sunday~Saturday까지 요일 전체 이름 표시
EEEEE M 한 글자 약어 요일 표시
e 4 1~7까지 주간 날짜 순서 표시
ee 04 01~07까지 주간 날짜 순서 표시
시기(period) a PM AM/PM 표시
시간(Hour) h 3 1~12까지 시각을 표시
hh 03 01~12까지 시각을 표시
H 15 1~24까지 24시간 시각을 표시
HH 15 01~24까지 24시간 시각을 표시
분(minute) m 36 0~59까지 분을 표시
mm 36 00~59까지 두자리로 분을 표시
초(second) s 44 0~59까지 초를 표시
ss 44 00~59까지 두자리로 초를 표시
지역(Zone) z GMT+09:00 타임존 표시
Z +0900 GMT 시간차 표시
// Timer.scheduledTimer을 이용해서 1초 단위로 화면의 시간을 갱신
class ViewController: UIViewController {
    
    let timeSelector:Selector = #selector(ViewController.updateTime)
    let interval = 1.0
    var count = 0

    @IBOutlet var lblCurrentTime: UILabel!
    @IBOutlet var lblPickerTime: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        Timer.scheduledTimer(timeInterval: interval, target: self, selector:timeSelector, userInfo: nil, repeats: true)
    }
    
    @objc func updateTime() {
        let date = Date()
        
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss EEEE"
        lblCurrentTime.text = "현재시각 : " + formatter.string(from: date)
        
    }
}

요약 
- DatePicker에 Timer.scheduledTimer()를 이용해서 현재 시간을 1초단위로 갱신할 수 있고, 특정시간에 이벤트를 발생시킬수 있다.  

 

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

아이폰 개발이 처음이라면, 정말 손가락으로 짚어주듯 하나하나 설명해주는 책

Do it! 스위프트로 아이폰 앱 만들기

본 책에 앞서 Swift 기초 문법에 대해서는 이미 배웠으리라 생각됩니다. 아직 struct, class, extension, 익명함수 등이 익숙하지 않아도 됩니다. Do it 입문서는 Xcode를 이용해서 아이폰 UI(Toolbar, Tabbar, 화면전환, StackView, Lable, Button 등) 를 그리는 것을 배우는 책이기 때문에 아이폰 개발을 위해 "기초문법 - UI - 주요기능코딩(SNS인증, Rest APi호출, DB연동 등)" 에서 UI는 화면을 그리는 것이라 문법이 약해도 상관없이 따라할 수 있습니다.

안드로이드와 비교해서 xml기반보다는 GUI기반으로 다가와 조금더 귀찮고, 실수로 잘못된 여지가 좀 있는 것 같지만 아직 Swift코딩에 익숙하지 않아도 빨리 따라할 수 있도록 잘 구성된 책입니다. 특히, 다른 책들은 예제소스를 따라하기에 반복적인 구성(버튼, 그림, 동일한 기능)으로 시간이 오래 걸리는데, 이책은 중복을 최소화하여 핵심 위주로 예제를 구성해서 따라하기 편하게 되어 있습니다. 

개정 5판이라 그런지, 따라해본 예제들은 모두 문법적 오류나 오타가 없었고, flutter처럼 버전에 따른 지원불가로 인한 오류도 없었습니다. 따라하는 과정에서 잘 안되는 경우는 UI를 그렸다가 지우고 다시 그리는 과정에서 Editor에 보이는 소스가 아닌 설정에 연결된 것을 지우지 않거나, 사진이미지를 너무 큰 것을 이용해서 제대로 보이지 않는 경우(이미지가 작은 사진들은 크기를 줄여서 프로젝트에 등록 필요)라 사진의 크기만 적절히 수정하면 됩니다. 

다른 책에서도 프로젝트 생성시 "Single View App"을 선택하라는 말이 있는데, 실제 Xcode화면에는 "Single View App"이란 단어가 없습니다. 그것이 가리키는 것은 바로 "App" 화면입니다. 이 책은 모든 과정에서 메뉴 선택하는 화면 하나하나를 보여주고 있어서 초보자들도 쉽게 배울 수 있습니다. 

 프로젝트 파일 살펴보기

가. AppDelegate : 앱의 실행주기를 관리하는 클래스 파일. 앱을 실행하거나 종료 또는 백그라운드를 실행할 때 하는 일을 관리

나. SceneDelegate : 사용자 인터페이스의 실행주기를 관리하는 클래스 파일

다. ViewController : 화면에 보이는 뷰에서 처리하는 내용의 소스가 담긴 클래스. 일반적으로 프로그래머는 이 파일에서 코딩하게 되며 뷰하나당 클래스 하나가 대응됩니다. 따라 스토리보드에서 여러 개의 뷰를 추가하면 뷰의 갯수 만큼 뷰 컨트롤러 클래스를 추가해야 합니다.

라. Main.storyboard : 앱의 내용을 시각적으로 쉽게 이해하고 프로그래밍할 수 있도록 그림으로 표현한 파일입니다. 스토리보드를 통해 화면에 보이는 내용 및 뷰 간의 연결 관계 등을 표현할 수 있습니다.

마. Assets.xcassets : 앱의 아이콘을 보관하는 저장소입니다.

바. LaunchScreen.storyboard : 앱이 실행될 때 잠시 나타나는 스플래시 화면을 만드는 스토리보드

사. Info.plist : 앱이 실행되는데 필요한 정보를 정하는 파일(권한 설정 등) : 권한 설정 기능을 따로 코딩하는 것이 아니라, 이 파일에 설정해 놓으면 앱에서 필요시 권한 추가 요청 기능이 실행됨(그러고 보면 아이폰에서 권한 요구하는 화면은 모두 동일한 것 같습니다.)

Objects Library 단축키 : command + shift + L 을 누르거나, 메인스토리 화면 위의 "+" 클릭하면 됩니다. Object를 많이 사용한다면 command + shift + L + option 를 누르면, Object를 선택하더라도 Object 팔레트 창이 닫히지 않아 계속 이용할 수 있습니다. 

아웃렛 변수/액션 함수 추가/삭제시 주의 사항

ViewController의 소스에서 삭제할지라도 실제 생성된 액션함수는 사라진 것이 아니라서 반드시 Show the connection inspector 에서 해당 객체를 삭제해줘야 합니다. 실제 이걸 삭제하지 않아서 오류가 발생하기도 합니다. 

iOS 시뮬레이터는 좌우 회전, 흔들기 효과, 멀티 터치(option키 누르고 클릭), GPS(수동입력) 테스트가 가능합니다. 다만, GPS를 이용한 실제 위치 취득, 전화 착신시 동작, 카메라 기능, 가속도 센서 는 테스트할 수 없습니다. 

 

 

 

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

카카오 REST API를 이용한 리스트 만들기

  1. REST API 키 발급
  2. http 통신 패키지 설정
  3. http 통신 확인
  4. jsondata ListView로 생성
  5. Scroll을 이용한 추가 가져오기

1. 카카오 API를 이용하기 위해, 개발자 사이트 등록 및 발급키 확인

카카오 개발자 사이트 : https://developers.kakao.com 

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

개발자 사이트에 가입후 개발할 어플리케이션까지 등록을 하면, 아래와 같이 4개의 키가 자동발급됩니다. 우리는 여기서 REST API 키를 이용할 계획입니다. 

개발자&nbsp; 사이트에서 REST API 키 확인

2. http 통신 패키지 설정

pubspec.yaml 파일의 dependencies 아래에 http를 추가합니다. 만약 null safety 지원 등으로 오류가 발생하면, https://pub.dev/packages/http/versions 에 접속해서 PC에 설치된 SDK 버전에 맞는 http버전을 입력하시면 됩니다. 참고로, 제 SDK 는 2.16.XX입니다. 

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.3

버전에 따라, Null safety 가 존재하고, 존재하지 않는 버전이 있음.

3. http 통신 확인

main.dart 파일에서 http를 import한 후, 플로팅 버튼을 클릭하면, 특정 페이지를 읽어 화면에 출력하는 페이지(골격)을 만들어봅니다. 비동기(async)를 이용해서 http.get(url)을 수행하면 됩니다. 

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HttpApp(),
    );
  }
}

class HttpApp extends StatefulWidget {

  @override
  State<StatefulWidget> createState() => _HttpApp();
}

class _HttpApp extends State<HttpApp> {

  String result="";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Http Example'),
      ),
      body: Container(
        child: Center(
          child: Text('$result'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          String url = 'http://www.google.com';
          var response = await http.get(Uri.parse(url));
          setState(() {
            result = response.body;
          } );
        },
        child: Icon(Icons.file_download),
      ),
    );
  }

 

4. JsonData를 ListView로 생성

먼저 jsonData를 가져올 함수를 비동기함수로 생성하고, url을 처음 발급받은 REST API 키를 이용해서 카카오 api 서버를 호출합니다. 

Future<String> getJSONData() async {
    var query = _editingController?.value.text;
    String url = "https://dapi.kakao.com/v3/search/book?target=title&query=${query}&page=${page}";
    var response = await http.get(Uri.parse(url),
                   headers: {"Authorization" : "KakaoAK cf********************6"});
    setState(() {
      var dataConvertedToJSON = json.decode(response.body);
      List result = dataConvertedToJSON['documents'];
      data.addAll(result);
    });
    return response.body;
  }

카카오 개발문서에 보면, Request할 때, 헤더값으로 Authorization에 Authorization: KakaoAK ${REST_API_KEY} 을 입력합니다. 도서, 비디오, 이미지 등 조회 내용에 따라 질의 항목, 결과 항목이 다르니 확인하시고 이용하시기 바랍니다.
카카오 개발사이트 > Docs > Daum Search >  REST API 

카카오 도서 관련 REST API, 조회건수는 default 값이 10

ListView.builder를 이용해서, Card() 안에 리스트 건에 대한 내용을 표시합니다. 개발하시다가 이미지가 화면을 벗어나면 오류 표시가 나는데, MediaQuery.of(context).size.width- 150 으로 하면, 화면에 모두 표시됩니다. 여기서 -150은 디바이스 전체 width에 들어가는 내용이, "이미지 + 글박스"로 구성되는데, 여기서 이미지 사이즈보다 큰 값을 입력하면 됩니다.

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

< 중략>

class _HttpApp extends State<HttpApp> {

  String result="";
  List data = [];
  TextEditingController? _editingController;    

  @override
  void initState() {
    super.initState();
    _editingController = new TextEditingController();    
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: TextField (
          controller: _editingController,
          style: TextStyle(color: Colors.white),
          keyboardType: TextInputType.text,
          decoration: InputDecoration(hintText: '검색어를 입력하세요'),
        )
      ),
      body: Container(
        child: Center(
          child: data.length ==0 ?
              Text('데이터가 없습니다.',style: TextStyle(fontSize: 20), textAlign: TextAlign.center,)
              : ListView.builder(itemBuilder: (context, index) {
                return Card(
                  child: Container(
                    child: Row(
                      children: <Widget>[
                        FadeInImage.assetNetwork(
                          height: 100,
                          width: 100,
                          fit: BoxFit.contain,
                          placeholder: 'images/spinner.gif',
                          image: data[index]['thumbnail'].toString(),
                        ),
                        //Image.network(data[index]['thumbnail'].toString(), height: 80, width: 80, fit:BoxFit.contain,),
                        Column(
                          mainAxisAlignment: MainAxisAlignment.start,
                            children: <Widget>[
                              //Text(data[index]['title'].toString(), overflow: TextOverflow.ellipsis,),
                              Container(
                                width: MediaQuery.of(context).size.width -120,
                                child: Text(data[index]['title'].toString(),  textAlign: TextAlign.left,),
                              ),
                              Text(data[index]['authors'].toString()),
                              Text(data[index]['sale_price'].toString()),
                              Text(data[index]['status'].toString()),
                            ]
                        )
                      ],
                    ),
                  ),
                );
          }, itemCount:data.length)
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: ()  {         
          data.clear();
          getJSONData();
        },
        child: Icon(Icons.file_download),
      ),
    );
  }
 
< 생략 >
  1. Scroll을 이용한 추가 가져오기

5. Scroll 을 이용한 추가 가져오기

책 소스에는 page를 추가하지 않아 다음 페이지를 가져오지 않는데, page가 없으면 항상 처음 10개만 가져옵니다. 그래서 새로운 검색어를 입력할 때마다 page를 초기화하고 계속 추가 조회할 때 마다 +1을 해줘야 합니다. 

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';


void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
    // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HttpApp(),
    );
  }
}

class HttpApp extends StatefulWidget {

  @override
  State<StatefulWidget> createState() => _HttpApp();
}

class _HttpApp extends State<HttpApp> {

  String result="";
  List data = [];
  TextEditingController? _editingController;
  ScrollController? _scollController;
  int page = 1;

  @override
  void initState() {
    super.initState();
    _editingController = new TextEditingController();
    _scollController = new ScrollController();

    _scollController?.addListener(() {
      if(_scollController!.offset >=
         _scollController!.position.maxScrollExtent &&
          !_scollController!.position.outOfRange) {
        print('bottom');
        page ++;
        getJSONData();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: TextField (
          controller: _editingController,
          style: TextStyle(color: Colors.white),
          keyboardType: TextInputType.text,
          decoration: InputDecoration(hintText: '검색어를 입력하세요'),
        )
      ),
      body: Container(
        child: Center(
          child: data.length ==0 ?
              Text('데이터가 없습니다.',style: TextStyle(fontSize: 20), textAlign: TextAlign.center,)
              : ListView.builder(itemBuilder: (context, index) {
                return Card(
                  child: Container(
                    child: Row(
                      children: <Widget>[
                        FadeInImage.assetNetwork(
                          height: 100,
                          width: 100,
                          fit: BoxFit.contain,
                          placeholder: 'images/spinner.gif',
                          image: data[index]['thumbnail'].toString(),
                        ),
                        //Image.network(data[index]['thumbnail'].toString(), height: 80, width: 80, fit:BoxFit.contain,),
                        Column(
                          mainAxisAlignment: MainAxisAlignment.start,
                            children: <Widget>[
                              //Text(data[index]['title'].toString(), overflow: TextOverflow.ellipsis,),
                              Container(
                                width: MediaQuery.of(context).size.width -120,
                                child: Text(data[index]['title'].toString(),  textAlign: TextAlign.left,),
                              ),
                              Text(data[index]['authors'].toString()),
                              Text(data[index]['sale_price'].toString()),
                              Text(data[index]['status'].toString()),
                            ]
                        )
                      ],
                    ),
                  ),
                );
          }, itemCount:data.length
          , controller: _scollController)
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: ()  {
          page = 1;
          data.clear();
          getJSONData();
        },
        child: Icon(Icons.file_download),
      ),
    );
  }

  Future<String> getJSONData() async {
    var query = _editingController?.value.text;
    String url = "https://dapi.kakao.com/v3/search/book?target=title&query=${query}&page=${page}";

    var response = await http.get(Uri.parse(url),
                   headers: {"Authorization" : "KakaoAK cf2***************************6"});

    print(response.body);

    setState(() {
      var dataConvertedToJSON = json.decode(response.body);
      List result = dataConvertedToJSON['documents'];
      data.addAll(result);
    });

    return response.body;
  }
}
요약
• REST API로 Json데이터를 가져와 ListView로 생성할 수 있다.
• REST API 호출시 Scroll을 이용하면서 page를 갱신해 추가 조회를 할 수 있다.
• 패키지를 가져올 때, 플러터 SDK 버전에 맞는 패키지 버전을 설정해야 한다.

 

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

플러터 문자를 숫자형으로 변환하려면, int.parse 

숫자를 문자형으로 변환하려면 .. $ 추가

 

int result = int.parse(widget1.value.text) + int.parse(widget2.value.text);
sum = '$result';

위의 소스처럼 문자열을 위젯의 값은 기본적으로 문자열로 인식되기 때문에, widget1.value가 아니라, widget1.value.text까지 해줘야 합니다. ​

그리고 int.parse()로 값을 숫자로 변환해서 읽으면 됩니다. ​

반대로, 숫자로된 값을 위젯에 대입하려면 오류가 발생합니다.(위젯값 타입 = 문자열) ​

그래서 $ 를 추가하여, sum = '$int형변수'; 로 하면 됩니다.

 

요약
• 문자를 숫자로 int.parse(문자열)
• 숫자를 문자로 '$숫자변수'
• 위젯의 값은 기본적으로 문자열로 인식되어 widget.value.text 로 읽음
Posted by 목표를 가지고 달린다
,

갤럭시탭(SM-T536 advanced) 액정 교체 후기.

  1. 액정을 인터넷에서 구매
  2. 전, 후면부 분리
  3. 기존 액정을 분리(열처리)
  4. 액정 교체
  5. 작업 결과 확인 및 청소

갤럭시탭 액정 파손 화면

아들 교육용으로 구매한 것이 액정이 파손되어 사용하고 있지 않아, 액정 교체를 해볼까 해서 인터넷에 찾아보니, 네이버 블로그에는 대부분 액정 교체 서비스 광고글이 98%였고, 유투브는 액정 교체 영상이 잘 나와 있습니다. 직접 해보면서 전문가가 아닌 일반 사람이 교체하는 입장에서 글을 써봅니다. 

결론적으론 제 태블릿은 사용을 못하게 되었습니다. 게다가 서비스센터에서도 고객이 임의로 손을 덴 제품에 대해서는 서비스 거부를 하기에 직접 하실 경우, 태블릿이 터치가 안될 수도 있다는 걸 유념하시기 바랍니다. 

1. 액정을 인터넷에서 구매

쇼핑몰에 액정 가격이  4~5만원 정도였습니다. 구매 내용물은 심플합니다. 액정(전자신호를 태블릿에 전달해주는 꼬리 달린) 과 도움될만 한 도구들입니다. 도구는 거의 사용하지 않고 집에 있는 일자 드라이버를 사용했습니다. 

액정과 작업 도구

2. 우선 후면부와 전면부를 분리해야 합니다. 

태블릿의 까만 부분과 흰색 부분을 분리하는 것인데, 생각보다 잘 안됩니다. 요령이 없어서 그런지 힘도 제법들어가구요. 저는 여러번 해보니, 태블릿 옆에 MicroSD카드 넣는 곳에 드라이버를 넣고 전면부(액정부분)을 밀어서 벌렸습니다. 그리고 전.후면부 사이를 드라이버로 벌리면서 분리했습니다. 유투브 영상을 보시면 이해가 잘 되실겁니다. 5cm라도 일단 틈을 만들면 그다음부터는 힘껏 하셔도 제품이 튼튼해서 부서지지 않고 분리됩니다. 

3. 기존 액정 분리

어떤 분들은 열처리 없이 잘 분리된다고 하고, 어떤 분은 열처리를 하면서 분리하는 영상이 있을 겁니다. 액정은 테두리 밑에 본드로 붙어 있는 상태입니다. 그래서 열처리 없이는 사실 엄청 어렵습니다. 저는 마침 집에서 교체를 하고 있어 하일라이트로 10cm 정도 떨어뜨려서 살짝 살짝 온도를 높이고 분리하고 또 온도를 높이고 분리했습니다. 액정을 분리하는 과정에서 액정이 깨지면서 미세한 유리가 조각이 엄청 나옵니다. ㅠㅠ 그래서 청소를 고려해서 주변을 깔끔하게 정리하고 시작하셔야 합니다.

액정을 분리하면서 액정이 더 깨지고 유리조각이 많이 생김

4. 액정 교체

교체는 쉽습니다. 기존 액정 분리후, 새로운 액정을 붙이고 뒤에 전선 한개만 연결하면 됩니다. 기존 액정을 잘 붙이기 위해 본드를 이용하시거나, 다시 열처리를 한 후 붙이시면 잘 붙습니다. 

액정의 신호를 태블릿으로 연결하는 전선

5. 작업 결과 확인 및 청소

후면부를 붙이기 전, 전원을 켜서 제대로 작동하는 확인 하시기 바랍니다. 정상적으로 터치가 되면 주변 유리들을 깔끔하게 정리하시기 바랍니다.

#. 주의 사항

저는 지금 교체 이후 터치 반응이 오지 않아, 열처리 과정에서 태블릿 본체에 문제가 생겼나? 구매한 액정이 문제가 있나?  싶어 삼성서비스 센터을 찾아갔습니다. 센터 직원은 고객이 임의로 작업한 제품에 대해서는 서비스 거부 권한이 있다고 체크해 주지 않습니다. 그래서 태블릿? 액정? 어디가 문제인지 알 수가 없었습니다. 액정의 전선은 태블릿에 잘 연결되어 있다고 육안으로 확인만 해주셨습니다. 

인터넷 블로그에 10~12만원 선에서 액정 교체 서비스 광고가 많습니다. 액정값을 제외하면 5~7만원의 공임비가 들어가는 것입니다. 직접 교체하면 5~7만원을 아낄 수도 있지만, 저처럼 잘못된 경우 오히려 이후 점검/수리비가 더 나올 수도 있어서 정말 필요한 태블릿이라면, 서비스를 맡기시는 것을 추천합니다. 

저는 나중에 사용하지 않는 태블릿이 생기면, 지금 사용 못하는 태블릿이랑 비교해보면서 무엇이 문제인지 확인해 보려합니다. 

 

 

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