GithubHelp home page GithubHelp logo

iosstudy's Introduction

iOSStudy

iOS 앱 개발 개인 스터디 공간입니다.

1. App Build Intro

버튼 클릭 시 같은 storyboard에 있는 ViewController를 화면에 띄워줍니다.

let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let detailViewController = storyBoard.instantiateViewController(identifier: "DetailViewController") as DetailViewController

self.present(detailViewController, animated: true, completion: nil)

2. Passing Data

2.1 ViewController 속성 값에 할당

// passing data (데이터를 넘겨주는 방법)
// 6가지

// 1. instance property

import UIKit

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

    @IBAction func moveToDetail(_ sender: Any) {
        let detailVC = DetailViewController(nibName: "DetailViewController", bundle: nil)
        detailVC.someString = "aaa 데이터"
        
        // 화면에 올라가기 전에 접근하면 에러가 발생
        // detailVC.someLabel.text = "bb"
        
        // present를 호출해야 화면에 올라갈 준비를 하기 때문에
        // present를 하기 전에 ui 요소에 접근을 하면 nil 에러가 발생
        self.present(detailVC, animated: true, completion: nil)
        
        // 보통 이런 방식으로 ui의 속성 값을 할당하지는 않는다.
        detailVC.someLabel.text = "bb"
        
    }
    
}
import UIKit

class DetailViewController: UIViewController {
    
    // ViewController instance가 생성되는 시점에
    // 해당 속성 값을 할당해 주고
    // 해당 값을 ui에 할당하는 시점은 viewDidLoad() 이후
    // 즉 ui가 메모리에 올라가고 나서 할당해 주는 방식을 사용한다.
    var someString = ""
    
    // instance가 생성되는 시점에는 nil이다.
    // 단, 화면에 UI들이 올라갈 수 있도록 준비 되는 시점에 ui가 메모리에 올라간다.
    // 즉, viewDidLoad가 될 때 생성된다.
    @IBOutlet weak var someLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // super.viewDidLoad()가 호출됐을 때 someLabel이 메모리에 올라가기 때문에 접근할 때 문제가 없다.
        someLabel.text = someString
    }

}

2.2 segue를 통한 데이터 전달

하나의 스토리보드에 여러 ViewController가 있을 때 사용

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

    // prepare를 사용하면 연결된 segue로 이동할 때 호출이 된다.
    // 따라서 원하는 segue를 찾아서 해당 viewCotroller의 속성 값에 값을 할당할 수 있다.
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "segueDetail" {
            if let detailVC = segue.destination as? SegueDetailViewController {
                detailVC.dataString = "adbc"
            }
        }
    }
}

2.3 instance를 통으로 넘겨서 데이터 전달하기

class ViewController: UIViewController {
    @IBAction func moveToInstance(_ sender: Any) {
        let detailVC = InstanceDetailViewController(nibName: "InstanceDetailViewController", bundle: nil)
        
        detailVC.mainVC = self
        
        self.present(detailVC, animated: true, completion: nil)
    }
}
class InstanceDetailViewController: UIViewController {
    
    var mainVC: ViewController?

    override func viewDidLoad() {
        super.viewDidLoad()

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

    @IBAction func sendDataMainVC(_ sender: Any) {
        mainVC?.dataLabel.text = "some data"
        self.dismiss(animated: true, completion: nil)
    }
}

ViewController에서 버튼 클릭 시 InstanceDetailViewController를 가지고 온 다음 InstanceDetailViewController에서 들고 있는 mainVC: ViewController에 self를 할당함으로써 InstanceDetailViewController에 ViewController의 instance를 그대로 전달하면서 데이터 전달을 진행합니다.

2.4 delegate를 사용해서 data를 전달하는 방법

// weak 키워드를 사용하기 위해서 AnyObject를 상속받아야 한다.
protocol DelegateDetailViewControllerDelegate: AnyObject {
    func passString(string: String)
}

class DelegateDetailViewController: UIViewController {
    
    // weak 타입으로 하는 이유
    // 해당 delegate의 정의가 내려지는 곳이 따로 있기 때문에
    // 그쪽에서 만들고 사용하고 끝나면 메모리 해제가 될 수 있도록 하기 위함
    weak var delegate: DelegateDetailViewControllerDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()

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

    @IBAction func passDataToMainVC(_ sender: Any) {
        delegate?.passString(string: "pass Data")
        self.dismiss(animated: true, completion: nil)
    }
}

delegate를 선언 방식으로 구현을 하고 실제로 버튼이 동작되는 부분에서 callback이 될 수 있도록 구현을 한다.

class ViewController: UIViewController {
    @IBOutlet weak var dataLabel: UILabel!

    @IBAction func moveToDelegate(_ sender: Any) {
        let detailVC = DelegateDetailViewController(nibName: "DelegateDetailViewController", bundle: nil)
        
        // 나 자신을 통으로 넘기는 것이 아니라 extension으로 구현한 구현부만 넘기게 된다.
        // 즉 DelegateDetailViewControllerDelegate 타입만 넘기는 것이다.
        detailVC.delegate = self
        self.present(detailVC, animated: true, completion: nil)
    }
}

extension ViewController: DelegateDetailViewControllerDelegate {
    func passString(string: String) {
        self.dataLabel.text = string
    }
}

delegate를 전달하는 부분은 위와 같이 extension을 활용하여 해당 객체가 protocol를 구현할 수 있도록 하고 self를 할당해 주면 된다.

2.5 Passing Data closure

Delegate 방식으로 다른 controller에 데이터를 전달하는 방식입니다.

class ClosureDetailViewController: UIViewController {
    
    var myClosure: ((String) -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func closurePassData(_ sender: Any) {
        myClosure?("closure string")
        self.dismiss(animated: true, completion: nil)
    }
}
class ViewController: UIViewController {
    @IBOutlet weak var dataLabel: UILabel!
    @IBAction func moveToClosure(_ sender: Any) {
        let detailVC = ClosureDetailViewController(nibName: "ClosureDetailViewController", bundle: nil)
        
        detailVC.myClosure = { str in
            self.dataLabel.text = str
        }
        
        self.present(detailVC, animated: true)
    }
}

2.6 Notification

Add observer by Notification.Name, and call observer by post function of NotificationCenter. And then, through the function added to observer, you can get notification that has key value pair.

class ViewController: UIViewController {
    var someString = ""
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let notificationName = Notification.Name("sendSomeString")
        // Don't call 'addObserver' twtice
        NotificationCenter.default.addObserver(self, selector: #selector(showSomeString), name: notificationName, object: nil)
        
        // add observer before(will) showing keyboard
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardDidShowNotification, object: nil)
        
        // add observer show keyboard completed
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil)
        
        NotificationCenter.default.removeObserver(self, name: notificationName, object: nil)
    }
  
    @objc func showSomeString(notification: Notification) {
        if let str = notification.userInfo?["str"] as? String {
            self.dataLabel.text = str;
        }
    }

    @objc func keyboardWillShow() {
        print("will show")
    }
    
    @objc func keyboardDidShow() {
        print("did show")
    }
    


    @IBAction func moveToNoti(_ sender: Any) {
        let detailVC = NotiDetailViewController(nibName: "NotiDetailViewController", bundle: nil)
        self.present(detailVC, animated: true, completion: nil)
    }
}
class NotiDetailViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
    }

    @IBAction func notiAction(_ sender: Any) {
        let notificationName = Notification.Name("sendSomeString")
        
        let strDic = ["str" : "noti string"]
        
        NotificationCenter.default.post(name: notificationName, object: nil, userInfo: strDic)
        self.dismiss(animated: true)
    }
}

3. Dispatch Queue

main thread vs work thread

// dispatch queue -> like task of C#
// create thread's' for management

import UIKit

class ViewController: UIViewController {
    
    // to check main thread working properly
    @IBOutlet weak var timerLabel: UILabel!

    @IBOutlet weak var finishLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            self.timerLabel.text = Date().timeIntervalSince1970.description
            
        }
    }
    
    // run on main thread
    // why ui job works in main thread?
    // -> when ui state updated, one thread has to manage it.
    // if multi thread manage it, you hardly to check this.
    // And app life cycle run on main thread.
    @IBAction func action1(_ sender: Any) {
        // finishLabel.text = "end"
        // simpleClousure {
        // finishLabel.text = "end_closure"
        // }
        multiThreadClosure {
            
            self.finishLabel.text = "end_multThread"
        }
    }
    
    func simpleClousure(complection: () -> Void) {
        
        for index in 0..<10 {
            // stop main thread temporarily.
            Thread.sleep(forTimeInterval: 0.2)
            print(index)
        }
        
        complection()
    }
    
    func multiThreadClosure(complection: @escaping () -> Void) {
        
        // run on work thread
        DispatchQueue.global().async {
            for index in 0..<10 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        // run on main thread
        // ui must run on main thread
        DispatchQueue.main.async {
            complection()
        }
    }
    
    
    @IBAction func action2(_ sender: Any) {
        // simpleAction2()
        multiAction2()
        
    }
    
    // run on sync -> run on one thread
    func simpleAction2() {
        let dispatchGroup = DispatchGroup()
        
        // create a thread
        let queue1 = DispatchQueue(label: "q1")
        
        queue1.async(group: dispatchGroup) {
            for index in 0..<10 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        // run on sync -> run on one thread
        queue1.async(group: dispatchGroup) {
            for index in 10..<20 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        // run on sync -> run on one thread
        queue1.async(group: dispatchGroup) {
            for index in 20..<30 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
    }
    
    // run on multi threads
    // run on async
    func multiAction2() {
        let dispatchGroup = DispatchGroup()
        
        // create multi threads
        let queue1 = DispatchQueue(label: "q1")
        let queue2 = DispatchQueue(label: "q2")
        let queue3 = DispatchQueue(label: "q3")
        
        // DispatchQoS.backgroud: the lowest priority
        // DispatchQoS.userInteractive: the highest priority.
        
        queue1.async(group: dispatchGroup, qos: .background) {
            // start job
            dispatchGroup.enter()
            DispatchQueue.global().async {
                for index in 0..<10 {
                    Thread.sleep(forTimeInterval: 0.2)
                    print(index)
                }
                // end job
                dispatchGroup.leave()
            }
        }
        
        queue2.async(group: dispatchGroup, qos: .userInteractive) {
            for index in 10..<20 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        queue3.async(group: dispatchGroup) {
            for index in 20..<30 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        // if all threads work finished, excute code block
        dispatchGroup.notify(queue: DispatchQueue.main) {
            print("end")
        }
    }
    
    @IBAction func action3(_ sender: Any) {
        // create multi threads
        let queue1 = DispatchQueue(label: "q1")
        let queue2 = DispatchQueue(label: "q2")
        
        queue1.sync {
            for index in 0..<10 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }

            queue1.sync {
                for index in 0..<10 {
                    Thread.sleep(forTimeInterval: 0.2)
                    print(index;)
                }
            }
        }
        
        queue2.sync {
            for index in 10..<20 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        print("aaaa")
    }
}

4. Settings Clone App

model class를 기반으로 table view를 구성합니다.

4.1 table cell 선택 시 원하는 view controller 등장시켰던 방법

// 현재 테이블에서 선택된 열(cell) 데이터
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    if indexPath.section == 0 {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileViewCell", for: indexPath) as! ProfileViewCell
        
        // indexPath.section: 현재 세션 번호
        // indexPath.row: 현재 세션에서 열의 번호
        cell.topTitle.text = settingModel[indexPath.section][indexPath.row].menuIttile
        cell.profileImageView.image = UIImage(systemName: settingModel[indexPath.section][indexPath.row].leftImageName)
        cell.bottomDescription.text = settingModel[indexPath.section][indexPath.row].subTitle
        
        return cell
    }
    
    let cell = tableView.dequeueReusableCell(withIdentifier: "MenuViewCell", for: indexPath) as! MenuViewCell
    
    cell.leftImageView.image = UIImage(systemName: settingModel[indexPath.section][indexPath.row].leftImageName)
    cell.leftImageView.tintColor = .red
    cell.middleTitle.text = settingModel[indexPath.section][indexPath.row].menuIttile
    cell.rightImageView.image = UIImage(systemName: settingModel[indexPath.section][indexPath.row].rightImageName ?? "")
    
    return cell
}

table view의 현재 세션 > 셀을 토대로 보여줘야 하는 view controller 등장시키는 방식으로 진행함.
-> 위 방법대로 진행되면 보여줘야 하는 셀이 하드코딩 되어 있기 때문에 좋은 방법으로 보여지지는 않습니다.
-> 현재 선택한 cell의 id 및 view controller를 맵핑하는 방식으로 수정하면 조금 더 깔끔한 구현이 가능할 것 같습니다.

5. Pan Gesture App

5.1 Pan Gesture 등록

init() {
    // frame: 기본 크기 설정
    super.init(frame: CGRect.zero)
    let pan = UIPanGestureRecognizer(target: self, action: #selector(dragging))
    self.addGestureRecognizer(pan)
}

// UIPanGestureRecognizer를 통해 PanGesture 값을 얻어올 수 있다.
@objc func dragging(pan: UIPanGestureRecognizer) {
    switch pan.state {
    // 누른 순간
    case .began:
        print("began")
    
    // 누르고 움직일 때
    case .changed:
        // 움직인 만큼의 값을 알려준다.
        // 현재 부모 뷰를 기준으로 얼만큼 이동했는지를 알려준다.
        let delta = pan.translation(in: self.superview)
        
        // 현재 view의 기준점을 center로 생각한다.
        var myPosition = self.center
        
        if dragType == .x {
            myPosition.x += delta.x
        } else if dragType == .y {
            myPosition.y += delta.y
        } else {
            myPosition.x += delta.x
            myPosition.y += delta.y
        }
        
        self.center = myPosition
        
        // 내가 움직인 만큼 이동을 끝낸 다음 pan을 초기화 해준다.
        pan.setTranslation(CGPoint.zero, in: self.superview)
        
    // ended: 움직임이 끝났을 때
    // cancelled: 누르고 멀리 이동하는 등, 이동이 취소 되었을 때
    case .ended, .cancelled:
        print("ended, cancelled")
        // minX: 해당 view의 좌측 상단 값
        // self.frame.minX < 0: 해당 view가 화면을 넘어가면
        if self.frame.minX < 0 {
            self.frame.origin.x = 0
        }
        if let hasSuperView = self.superview {
            // maxX: 해당 view의 우측 상단 값
            if self.frame.maxX > hasSuperView.frame.maxX {
                self.frame.origin.x = hasSuperView.frame.maxX - self.bounds.width
            }
        }

    default:
        break
    }
}

6. OnBoarding View

class OnBoardingPageViewController: UIPageViewController {
    
    var pages = [UIViewController]()
    var bottomButtonMargin: NSLayoutConstraint?
    var pageControl = UIPageControl()
    
    let startIndex = 0
    var currentIndex = 0 {
        didSet {
            pageControl.currentPage = currentIndex
        }
    }
    
    func makePageVC() {
        let itemVC1 = OnBoardingItemViewController.init(nibName: "OnBoardingItemViewController", bundle: nil)
        itemVC1.mainText = "Focus on your ideal buyer"
        itemVC1.topImage = UIImage(named: "onboarding1")
        itemVC1.subText = "When you write..."
        
        let itemVC2 = OnBoardingItemViewController.init(nibName: "OnBoardingItemViewController", bundle: nil)
        itemVC2.mainText = "Entice with benefits"
        itemVC2.topImage = UIImage(named: "onboarding2")
        itemVC2.subText = "When we sell our .."
        
        let itemVC3 = OnBoardingItemViewController.init(nibName: "OnBoardingItemViewController", bundle: nil)
        itemVC3.mainText = "Avoid yeah.."
        itemVC3.topImage = UIImage(named: "onboarding3")
        itemVC3.subText = "When we're stuck..."
        
        pages.append(itemVC1)
        pages.append(itemVC2)
        pages.append(itemVC3)
        
        // 처음으로 나와야 하는 페이지 설정
        setViewControllers([itemVC1], direction: .forward, animated: true, completion: nil)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.makePageVC()
        self.makeBottomButton()
        self.makePageControl()
        
        self.dataSource = self
        self.delegate = self
    }
    
    func makeBottomButton() {
        print("make button")
        let button = UIButton()
        button.setTitle("확인", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.backgroundColor = UIColor.systemBlue
        
        // touchUpInside: 버튼을 클릭 후 땠을 때 호출
        button.addTarget(self, action: #selector(dismissPageVC), for: .touchUpInside)
        
        // 버튼을 view에 올리는 과정
        self.view.addSubview(button)
        // auto layout 설정을 위해서는 아래 값이 false로 설정이 되어 있어야 한다.
        button.translatesAutoresizingMaskIntoConstraints = false
        
        // code로 auto layout 설정
        // equalTo: 어디를 기준으로 anchor를 심을 것인지
        // self.view의 의미: 현재 View는 OnBoardingPageViewController이기 때문에 버튼이 부모 view를 기준으로 엥커를 심는 것이다.
        button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        button.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
        button.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
        button.heightAnchor.constraint(equalToConstant: 50).isActive = true
        
        bottomButtonMargin = button.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: 100)
        bottomButtonMargin?.isActive = true
        hideButton()
    }
    
    func makePageControl() {
        self.view.addSubview(pageControl)
        pageControl.translatesAutoresizingMaskIntoConstraints = false
        // 선택한 페이지의 점색상
        pageControl.currentPageIndicatorTintColor = .black
        // 선택되지 않은 페이지의 점색상
        pageControl.pageIndicatorTintColor = .lightGray
        // 점의 갯수
        pageControl.numberOfPages = pages.count
        pageControl.currentPage = startIndex
        
        // 점을 클릭 시 아무런 동작도 안하게끔 만들어줌
//        pageControl.isUserInteractionEnabled = false
        
        pageControl.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -80).isActive = true
        pageControl.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        
        // button의 이벤트 등록과 동일한 구현
        pageControl.addTarget(self, action: #selector(pageControlTapped), for: .valueChanged)
    }
    
    @objc func dismissPageVC() {
        self.dismiss(animated: true)
    }
    
    @objc func pageControlTapped(sender: UIPageControl) {
        
        // 1 -> 2 페이지로 이동
        if sender.currentPage > self.currentIndex {
            self.setViewControllers([pages[sender.currentPage]], direction: .forward, animated: true, completion: nil)

        } else {
            self.setViewControllers([pages[sender.currentPage]], direction: .reverse, animated: true, completion: nil)

        }
        
        currentIndex = sender.currentPage
        
        buttonPresentationStyle()
    }
}

extension OnBoardingPageViewController: UIPageViewControllerDataSource {
    // 현재 페이지에서 이전 페이지로 이동했을 시 나오는 페이지 정의
    // pageViewController: 현재 페이지
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard let currentIndex = pages.firstIndex(of: viewController) else {
            return nil
        }
        
        self.currentIndex = currentIndex
        
        if currentIndex == 0 {
            return pages.last
        } else {
            return pages[currentIndex - 1]
        }
        
    }
    
    // 현재 페이지에서 이후 페이지로 이동했을 시 나오는 페이지 정의
    // pageViewController: 현재 페이지
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let currentIndex = pages.firstIndex(of: viewController) else {
            return nil
        }
        
        if currentIndex == pages.count - 1 {
            return pages.first
        } else {
            return pages[currentIndex + 1]
        }
    }
}

extension OnBoardingPageViewController: UIPageViewControllerDelegate {
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        guard let currentVC = pageViewController.viewControllers?.first else {
            return
        }
        
        guard let currentIndex = pages.firstIndex(of: currentVC) else {
            return
        }
        self.currentIndex = currentIndex
        
        buttonPresentationStyle()

    }
    func buttonPresentationStyle() {
        if currentIndex == pages.count - 1 {
            self.showButton()
        } else {
            self.hideButton()
        }
        
        // 버튼이 등장하고 사라지는 첫 번째 방법
//        UIView.animate(withDuration: 0.5, delay: 0) {
//            self.view.layoutIfNeeded()
//        }
        
        // 버튼이 등장하고 사라지는 두 번째 방법
        UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.25, delay: 0, options: [.curveEaseInOut], animations: {
            // 즉시 변경 -> 0.5초 동안의 시간을 두고
            self.view.layoutIfNeeded()
        }, completion: nil)
    }
    
    func showButton() {
        bottomButtonMargin?.constant = 0
    }
    
    func hideButton() {
        bottomButtonMargin?.constant = 100
    }
}

7. Photo Gallery App

7.1 갤러리 접근 권한 확인 방법

@objc func checkPermission() {
    // authorized: 모두 허용
    // limited: 일부 허용
    if PHPhotoLibrary.authorizationStatus() == .authorized ||
        PHPhotoLibrary.authorizationStatus() == .limited {
        DispatchQueue.main.async {
            // UI를 보여주는 것은 메인 쓰레드에서 동작을 해야 한다.
            self.showGallery()
        }
        
    // denied: 거절
    } else if PHPhotoLibrary.authorizationStatus() == .denied {
        DispatchQueue.main.async {
            self.showAuthorizationDeniedAlert()
        }
        
    // notDetermined: 한 번도 권한을 물어보지 않은 상태
    } else if PHPhotoLibrary.authorizationStatus() == .notDetermined {
        PHPhotoLibrary.requestAuthorization { status in
            self.checkPermission()
        }
    }
}

func showAuthorizationDeniedAlert() {
    let alert = UIAlertController(title: "포토라이브러리 접근 권한을 활성화 해주세요.", message: nil, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "닫기", style: .cancel, handler: nil))
    alert.addAction(UIAlertAction(title: "설정으로 가기", style: .default, handler: { action in
        // 앱 설정 열기
        guard let url = URL(string: UIApplication.openSettingsURLString) else {
            return
        }
        if UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        }
    }))
    self.present(alert, animated: true, completion: nil)
}

@objc func showGallery() {
    let library = PHPhotoLibrary.shared()
    var configuration = PHPickerConfiguration(photoLibrary: library)
    configuration.selectionLimit = 10
    let picker = PHPickerViewController(configuration: configuration)
    
    picker.delegate = self
    present(picker, animated: true, completion: nil)
}

@objc func refresh() {
    self.photoCollectionView.reloadData()
}

7.2 갤러리에서 선택한 이미지 불러오기

class ViewController: UIViewController {
    var fetchResults: PHFetchResult<PHAsset>?
}

// table view이 기본 형태
// 연결된 table view의 갯수 및 현재 cell에 대해서 정의해 주고 있다.
extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.fetchResults?.count ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell
        if let asset = self.fetchResults?[indexPath.row] {
            cell.loadImage(asset: asset)
        }
        
        return cell
    }
}

// 사진 선택(add) 후 콜백처리
extension ViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        
        let identifiers = results.map {
             $0.assetIdentifier ?? ""
        }
        self.fetchResults = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil)
        // 갯수가 업데이트 됨
        self.photoCollectionView.reloadData()
        
        self.dismiss(animated: true)
    }
}

class PhotoCell: UICollectionViewCell {
    
    @IBOutlet weak var photoImageView: UIImageView! {
        didSet {
            photoImageView.contentMode = .scaleAspectFill
        }
    }
    
    func loadImage(asset: PHAsset) {
        let imageManager = PHImageManager()
        let scale = UIScreen.main.scale
        // scale을 곱하는 이유 -> 핸드폰 해상도와 맞추기 위해서
        let imageSize = CGSize(width: 150 * scale, height: 150 * scale)
        
        let options = PHImageRequestOptions()
        // .highQualityFormat: 이미지를 요청할 때 고화질로 요청
        // .fastFormat: 이미지를 요청할 때 저화질로 요청
        // .opportunistic: 저화질 고화질 모두 요청
        options.deliveryMode = .opportunistic
        
        self.photoImageView.image = nil
        
        imageManager.requestImage(for: asset, targetSize: imageSize, contentMode: .aspectFill, options: options) { image, info in
            
            if (info?[PHImageResultIsDegradedKey] as? Bool) == true {
                // 저화질
            } else {
                // 고화질
            }
            self.photoImageView.image = image
            
        }
    }
}

8. Movie App

| | | |

8.1 HTTP Request

import Foundation

enum MovieAPIType {
    case justURL(urlString: String)
    case searchMovie(querys: [URLQueryItem])
}

enum MovieAPIError: Error {
    case badRUL
}

class NetworkLayer {
    
    // only url
    // url + param
    
    typealias NetworkComplection = (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void
    
    func request(type: MovieAPIType, complection: @escaping NetworkComplection) {
        let sessionConfig = URLSessionConfiguration.default
        let session = URLSession(configuration: sessionConfig)
        
        do {
            let request = try buildRequest(type: type)
            
            session.dataTask(with: request) { data, response, error in
                print((response as! HTTPURLResponse).statusCode)
                complection(data, response, error)
            }.resume()
            session.finishTasksAndInvalidate()
            
        } catch {
            print(error)
        }
    }
    
    private func buildRequest(type: MovieAPIType) throws -> URLRequest {
        switch type {
        case .justURL(urlString: let urlString):
            guard let hasURL = URL(string: urlString) else {
                throw MovieAPIError.badRUL
            }
            
            var request = URLRequest(url: hasURL)
            request.httpMethod = "GET"
            
            return request
            
        case .searchMovie(querys: let querys):
            var components = URLComponents(string: "https://itunes.apple.com/search")
            components?.queryItems = querys
            guard let hasUrl = components?.url else {
                throw MovieAPIError.badRUL
            }
            
            var request = URLRequest(url: hasUrl)
            request.httpMethod = "GET"
            
            return request
        }
    }
}

8.2 Data Handling

class ViewController: UIViewController {
    
    var movieModel: MovieModel?
    var term = ""
    
    var networkLayer = NetworkLayer()

    @IBOutlet weak var searchBar: UISearchBar!
    @IBOutlet weak var movieTableView: UITableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        
        movieTableView.delegate = self
        movieTableView.dataSource = self
        searchBar.delegate = self
    }
    
    func loadImage(urlString: String, complection: @escaping (UIImage?) -> Void)  {
        networkLayer.request(type: .justURL(urlString: urlString)) { data, response, error in
            if let hasData = data {
                complection(UIImage(data: hasData))
                return
            }
            complection(nil)
        }
    }
    
    func requestMovieAPI() {
        let term = URLQueryItem(name: "term", value: term)
        let media = URLQueryItem(name: "media", value: "movie")
        let querys = [term, media]
        
        networkLayer.request(type: .searchMovie(querys: querys)) { data, response, error in
            if let hasData = data {
                do {
                    self.movieModel = try JSONDecoder().decode(MovieModel.self, from: hasData)
                    print(self.movieModel ?? "no data")
                    
                    DispatchQueue.main.async {
                        // 데이터를 갱신
                        self.movieTableView.reloadData()
                    }
                    
                } catch {
                    print(error)
                }
            }
        }
    }
}

import Foundation

// Codable == serializable과 동일
struct MovieModel: Codable {
    let resultCount: Int
    let results: [MovieResult]
}

struct MovieResult: Codable {
    let trackName: String?
    let previewUrl: String?
    let image: String?
    let shortDescription: String?
    let longDescription: String?
    let trackPrice: Double?
    let currency: String?
    let releaseDate: String?
    
    // artworkUrl100 대신 image를 쓰고 싶을 때
    enum CodingKeys: String, CodingKey {
        case image = "artworkUrl100"
        case trackName
        case previewUrl
        case shortDescription
        case longDescription
        case trackPrice
        case currency
        case releaseDate
        
    }
}

9. Todo App

9.1 Create Core Data

func saveTodo() {
    guard let entityDescription = NSEntityDescription.entity(forEntityName: "TodoList", in: context) else {
        return
    }
    
    guard let object = NSManagedObject(entity: entityDescription, insertInto: context) as? TodoList else {
        return
    }
    
    object.title = titleTextField.text
    object.date = Date()
    object.uuid = UUID()
    
    object.priorityLevel = priority?.rawValue ?? PriorityLevel.level1.rawValue
    
    let appDelegate = (UIApplication.shared.delegate as! AppDelegate)
    appDelegate.saveContext()
}

9.2 Read Core Data

func fetchDate() {
    let fetchRequest: NSFetchRequest<TodoList> = TodoList.fetchRequest()
    let context = appdelegate.persistentContainer.viewContext
    
    do {
        self.todoList = try context.fetch(fetchRequest)
        
    } catch {
        print(error)
    }
}

// table view
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "TodoCell", for: indexPath) as! TodoCelll
    cell.topTitleLabel.text = todoList[indexPath.row].title
    if let hasDate = todoList[indexPath.row].date {
        let formatter = DateFormatter()
        formatter.dateFormat = "MM-dd hh:mm:ss"
        let dateString = formatter.string(from: hasDate)
        
        cell.dateLabel.text = dateString
    } else {
        cell.dateLabel.text = ""
    }
    // 로컬 db에 있는 color 값을 가지고 온다.
    let priority = todoList[indexPath.row].priorityLevel
    
    let priorityColor = PriorityLevel(rawValue: priority)?.color
    cell.prioirtyView.backgroundColor = priorityColor
    cell.prioirtyView.layer.cornerRadius = cell.prioirtyView.bounds.height / 2

    return cell
}

9.3 Update Core Data

func updateTodo() {
    guard let hasData = selectedTodoList else {
        return
    }
    
    let fectchRequest: NSFetchRequest<TodoList> = TodoList.fetchRequest()
    
    guard let hasUUID = hasData.uuid else {
        return
    }
    
    // 선택한 uuid의 값만 가지고 올 수 있따.
    fectchRequest.predicate = NSPredicate(format: "uuid = %@", hasUUID as CVarArg)
    
    do {
        let loadedData = try context.fetch(fectchRequest)
        loadedData.first?.title = titleTextField.text
        loadedData.first?.date = Date()
        loadedData.first?.priorityLevel = self.priority?.rawValue ?? PriorityLevel.level1.rawValue
        appDelegate.saveContext()
        
    } catch {
        print(error)
    }
}

9.4 Delete Core Data

@IBAction func deleteTod(_ sender: UIButton) {
    guard let hasData = selectedTodoList else {
        return
    }
    
    let fectchRequest: NSFetchRequest<TodoList> = TodoList.fetchRequest()
    
    guard let hasUUID = hasData.uuid else {
        return
    }
    
    // 선택한 uuid의 값만 가지고 올 수 있따.
    fectchRequest.predicate = NSPredicate(format: "uuid = %@", hasUUID as CVarArg)
    
    do {
        let loadedData = try context.fetch(fectchRequest)
        if let loadedFirstData = loadedData.first {
            context.delete(loadedFirstData)
            appDelegate.saveContext()
        }
        
    } catch {
        print(error)
    }
    
    
    delegate?.didFinishSaveData()
    self.dismiss(animated: true)
}

iosstudy's People

Contributors

joseph-cha avatar

Watchers

 avatar

iosstudy's Issues

Dispatch Queue

main thread vs work thread

// dispatch queue -> like c# of task
// create thread's' for manage

import UIKit

class ViewController: UIViewController {
    
    // to check main thread work properly
    @IBOutlet weak var timerLabel: UILabel!

    @IBOutlet weak var finishLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            self.timerLabel.text = Date().timeIntervalSince1970.description
            
        }
    }
    
    // run on main thread
    // why ui job work in main thread?
    // -> when ui state updated, one thread has to manage that.
    // if multi thread manage that, you hardly to check this.
    // And app life cycle run on main thread.
    @IBAction func action1(_ sender: Any) {
        // finishLabel.text = "end"
        // simpleClousure {
        // finishLabel.text = "end_closure"
        // }
        multiThreadClosure {
            
            self.finishLabel.text = "end_multThread"
        }
    }
    
    func simpleClousure(complection: () -> Void) {
        
        for index in 0..<10 {
            // stop main thread temporarily.
            // because of stopping main thread, it couldn't run function of complection.
            Thread.sleep(forTimeInterval: 0.2)
            print(index)
        }
        
        complection()
    }
    
    func multiThreadClosure(complection: @escaping () -> Void) {
        
        // run on work thread
        DispatchQueue.global().async {
            for index in 0..<10 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        // run on main thread
        // ui must run on main thread
        DispatchQueue.main.async {
            complection()
        }
    }
    
    
    @IBAction func action2(_ sender: Any) {
//        simpleAction2()
        multiAction2()
        
    }
    
    // run on sync -> run on one thread
    func simpleAction2() {
        let dispatchGroup = DispatchGroup()
        
        // create a thread
        let queue1 = DispatchQueue(label: "q1")
        
        queue1.async(group: dispatchGroup) {
            for index in 0..<10 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        // run on sync -> run on one thread
        queue1.async(group: dispatchGroup) {
            for index in 10..<20 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        // run on sync -> run on one thread
        queue1.async(group: dispatchGroup) {
            for index in 20..<30 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
    }
    
    // run on multi threads
    // run on async
    func multiAction2() {
        let dispatchGroup = DispatchGroup()
        
        // create multi threads
        let queue1 = DispatchQueue(label: "q1")
        let queue2 = DispatchQueue(label: "q2")
        let queue3 = DispatchQueue(label: "q3")
        
        // DispatchQoS.backgroud: the lowest priority
        // DispatchQoS.userInteractive: the highest priority.
        
        queue1.async(group: dispatchGroup, qos: .background) {
            // set to start job manualy
            dispatchGroup.enter()
            DispatchQueue.global().async {
                for index in 0..<10 {
                    Thread.sleep(forTimeInterval: 0.2)
                    print(index)
                }
                // set to end job manualy
                dispatchGroup.leave()
            }
        }
        
        queue2.async(group: dispatchGroup, qos: .userInteractive) {
            for index in 10..<20 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        queue3.async(group: dispatchGroup) {
            for index in 20..<30 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        // if all threads work finished, excute code block
        dispatchGroup.notify(queue: DispatchQueue.main) {
            print("end")
        }
    }
    
    @IBAction func action3(_ sender: Any) {
        
        // don't use that(main thread do not run on main thread).
        DispatchQueue.main.sync {
            
        }
        
        // create multi threads
        let queue1 = DispatchQueue(label: "q1")
        let queue2 = DispatchQueue(label: "q2")
        
        queue1.sync {
            for index in 0..<10 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
            // deadlock -> waiting for each other jobs until the other's.
            // previous job is not complected. so this job waits for previous job.
            // next job will run sync. so when all queue1 thread's job finished, next job will be excuted by queue1 thread.
            // that is why occur dead lock.
            queue1.sync {
                for index in 0..<10 {
                    Thread.sleep(forTimeInterval: 0.2)
                    print(index)
                }
            }
        }
        
        queue2.sync {
            for index in 10..<20 {
                Thread.sleep(forTimeInterval: 0.2)
                print(index)
            }
        }
        
        print("aaaa")
    }
}

Passing Data_3 (Instance를 통으로 넘겨서 데이터를 전달하는 방법)

instance를 통으로 넘겨서 데이터 전달하기

class ViewController: UIViewController {
    @IBAction func moveToInstance(_ sender: Any) {
        let detailVC = InstanceDetailViewController(nibName: "InstanceDetailViewController", bundle: nil)
        
        detailVC.mainVC = self
        
        self.present(detailVC, animated: true, completion: nil)
    }
}
class InstanceDetailViewController: UIViewController {
    
    var mainVC: ViewController?

    override func viewDidLoad() {
        super.viewDidLoad()

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

    @IBAction func sendDataMainVC(_ sender: Any) {
        mainVC?.dataLabel.text = "some data"
        self.dismiss(animated: true, completion: nil)
    }
}

ViewController에서 버튼 클릭 시 InstanceDetailViewController를 가지고 온 다음 InstanceDetailViewController에서 들고 있는 mainVC: ViewController에 self를 할당함으로써 InstanceDetailViewController에 ViewController의 instance를 그대로 전달하면서 데이터 전달을 진행합니다.

Passing Data_4 delegate를 사용해서 data를 전달하는 방법

delegate를 사용해서 data를 전달하는 방법

// weak 키워드를 사용하기 위해서 AnyObject를 상속받아야 한다.
protocol DelegateDetailViewControllerDelegate: AnyObject {
    func passString(string: String)
}

class DelegateDetailViewController: UIViewController {
    
    // weak 타입으로 하는 이유
    // 해당 delegate의 정의가 내려지는 곳이 따로 있기 때문에
    // 그쪽에서 만들고 사용하고 끝나면 메모리 해제가 될 수 있도록 하기 위함
    weak var delegate: DelegateDetailViewControllerDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()

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

    @IBAction func passDataToMainVC(_ sender: Any) {
        delegate?.passString(string: "pass Data")
        self.dismiss(animated: true, completion: nil)
    }
}

delegate를 선언 방식으로 구현을 하고 실제로 버튼이 동작되는 부분에서 callback이 될 수 있도록 구현을 한다.

class ViewController: UIViewController {
    @IBOutlet weak var dataLabel: UILabel!

    @IBAction func moveToDelegate(_ sender: Any) {
        let detailVC = DelegateDetailViewController(nibName: "DelegateDetailViewController", bundle: nil)
        
        // 나 자신을 통으로 넘기는 것이 아니라 extension으로 구현한 구현부만 넘기게 된다.
        // 즉 DelegateDetailViewControllerDelegate 타입만 넘기는 것이다.
        detailVC.delegate = self
        self.present(detailVC, animated: true, completion: nil)
    }
}

extension ViewController: DelegateDetailViewControllerDelegate {
    func passString(string: String) {
        self.dataLabel.text = string
    }
}

delegate를 전달하는 부분은 위와 같이 extension을 활용하여 해당 객체가 protocol를 구현할 수 있도록 하고
self를 할당해 주면 된다.

Passing Data_5 closure

Passing Data closure

Delegate 방식으로 다른 controller에 데이터를 전달하는 방식입니다.

closure view controller

class ClosureDetailViewController: UIViewController {
    
    var myClosure: ((String) -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func closurePassData(_ sender: Any) {
        myClosure?("closure string")
        self.dismiss(animated: true, completion: nil)
    }
}

main view controller

class ViewController: UIViewController {
    @IBOutlet weak var dataLabel: UILabel!
    @IBAction func moveToClosure(_ sender: Any) {
        let detailVC = ClosureDetailViewController(nibName: "ClosureDetailViewController", bundle: nil)
        
        detailVC.myClosure = { str in
            self.dataLabel.text = str
        }
        
        self.present(detailVC, animated: true)
    }
}

Passing Data_6 notification

Passing Data_6 notification

Add observer by Notification.Name, and call observer by post function of NotificationCenter.
And then, through the function added to observer, you can get notification that has key value pair.

Main ViewController

class ViewController: UIViewController {
    var someString = ""
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let notificationName = Notification.Name("sendSomeString")
        // Don't call 'addObserver' twtice
        NotificationCenter.default.addObserver(self, selector: #selector(showSomeString), name: notificationName, object: nil)
        
        // add observer before(will) showing keyboard
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardDidShowNotification, object: nil)
        
        // add observer show keyboard completed
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil)
        
        NotificationCenter.default.removeObserver(self, name: notificationName, object: nil)
    }
  
    @objc func showSomeString(notification: Notification) {
        if let str = notification.userInfo?["str"] as? String {
            self.dataLabel.text = str;
        }
    }

    @objc func keyboardWillShow() {
        print("will show")
    }
    
    @objc func keyboardDidShow() {
        print("did show")
    }
    


    @IBAction func moveToNoti(_ sender: Any) {
        let detailVC = NotiDetailViewController(nibName: "NotiDetailViewController", bundle: nil)
        self.present(detailVC, animated: true, completion: nil)
    }
}
class NotiDetailViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
    }

    @IBAction func notiAction(_ sender: Any) {
        let notificationName = Notification.Name("sendSomeString")
        
        let strDic = ["str" : "noti string"]
        
        NotificationCenter.default.post(name: notificationName, object: nil, userInfo: strDic)
        self.dismiss(animated: true)
    }
    
}

Passing Data_2

segue를 통한 데이터 전달

하나의 스토리보드에 여러 ViewController가 있을 때 사용

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

    // prepare를 사용하면 연결된 segue로 이동할 때 호출이 된다.
    // 따라서 원하는 segue를 찾아서 해당 viewCotroller의 속성 값에 값을 할당할 수 있다.
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "segueDetail" {
            if let detailVC = segue.destination as? SegueDetailViewController {
                detailVC.dataString = "adbc"
            }
        }
    }
}

segue 생성 방법

image
image

버튼을 생성 후 같은 스토리보드 안에 있는 View Controller를 오른쪽 마우스로 끌고 와서 Action segue의 show를 클릭하면 segue 연결이 완료된다.

그리고 아래와 같이 segue의 id 값을 원하는 값(위 예제에서는 segueDetail 값으로 설정해 주면 된다.

image

App Build Intro

주요 내용 요약

AppDelegate

App Life Cycle 관리

//
//  AppDelegate.swift
//  App_Start
//
//  Created by 차동훈 on 2022/11/01.
//

// App Life Cycle 관리
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {



    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    // MARK: UISceneSession Lifecycle
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}

UIViewController의 주요 라이프 사이클 요소

//  DetailViewController.swift
//  App_Start
//
//  Created by 차동훈 on 2022/11/01.
//
import UIKit

// UIViewController의 라이프 사이클 중요 요소
class DetailViewController: UIViewController {

    // instance 된 다음 화면에 올릴 준비가 되었을 때
    override func viewDidLoad() {
        super.viewDidLoad()

    }

    // 화면에 나오기 직전
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }

    // 화면에 나온 후
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }

    // 화면에 사라지기 직전
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
    }

    // 화면에 사라지고 난 다음
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
    }
}

샘플 앱 버튼 연결 및 화면 이동

//
//  ViewController.swift
//  App_Start
//
//  Created by 차동훈 on 2022/11/01.
import UIKit

// 실행 -> command + R
// fileprivate: 해당 파일에서만 접근 가능
fileprivate enum SomeStype {
    case aa
}

class ViewController: UIViewController {

    // 이렇게 클래스 내부에 선언하면 해당 클래스에서만 사용 가능
    struct SomeStruct {
        var aaa = ""
    }

    // @IBOutlet -> 화면이랑 연결된 변수라는 의미
    @IBOutlet weak var testButton: UIButton!

    // @IBAction -> 화면이랑 연결된 기능과 연결됨
    // 즉 버튼을 클릭 했을 때 연동되는 액션
    @IBAction func doSomething(_ sender: Any) {
        // .orange가 가능한 이유는 backgroundColor 타입이 UIColor로 고정되어 있기 때문에 타입 추론이 가능해서이다.
        testButton.backgroundColor = .orange
        let storyBoard = UIStoryboard(name: "Main", bundle: nil)
        let detailViewController = storyBoard.instantiateViewController(identifier: "DetailViewController") as DetailViewController

        self.present(detailViewController, animated: true, completion: nil)

    }

    // 화면이 메모리에 올라 갔을 때
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        testButton.backgroundColor = UIColor.red
    }
}

아이폰 Setting Clone

Setting Clone

전체 소스

ViewController.swift

import UIKit

// 화면의 UI를 표시, 화면 이동을 관리함
class ViewController: UIViewController {
    
    // [[]]: 2차원 배열
    var settingModel = [[SettingModel]]()
    
    @IBOutlet weak var settingTableView: UITableView!

    func makeData() {
        settingModel.append(
            [SettingModel(leftImageName: "person.circle", menuIttile: "Sign in to your iPhone", subTitle: "Set up iCoud, the App Store, and more", rightImageName: nil)]
        )
        
        settingModel.append(
            [SettingModel(leftImageName: "gear", menuIttile: "General", subTitle: nil, rightImageName: "chevron.right"),
            SettingModel(leftImageName: "person.fill", menuIttile: "Accessibility", subTitle: nil, rightImageName: "chevron.right"),
            SettingModel(leftImageName: "hand.raised.fill", menuIttile: "Privacy", subTitle: nil, rightImageName: "chevron.right")]
        )
    }
    
    // like OnEnable of Unity MonoBehaviour
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.navigationController?.navigationBar.prefersLargeTitles = true
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        settingTableView.delegate = self
        settingTableView.dataSource = self
        settingTableView.backgroundColor = UIColor(white: 245/255, alpha: 1)
        
        // ProfileViewCell에서 설정된 ID 값을 토대로 가지고 옴.
        settingTableView.register(UINib(nibName: "ProfileViewCell", bundle: nil), forCellReuseIdentifier: "ProfileViewCell")
        settingTableView.register(UINib(nibName: "MenuViewCell", bundle: nil), forCellReuseIdentifier: "MenuViewCell")
        
        // 네비게이션 컨트롤로의 타이틀 값 설정
        self.title = "Settings"
        // 상단 bar 영역
        self.view.backgroundColor = UIColor(white: 245/255, alpha: 1)
        
        makeData()
    }
}

// table view를 구성하는 기본적인 protocol 구현 방법
extension ViewController: UITableViewDelegate, UITableViewDataSource {
    
    // 세션의 갯수
    func numberOfSections(in tableView: UITableView) -> Int {
        return settingModel.count
    }
    
    // 현재 세션이 몇 개의 열을 가지고 있는지 결정
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // 현재 세션의 갯수만큼 셀이 출력 될 수 있도록
        return settingModel[section].count
    }
    
    
    // 테이블 뷰 셀을 클릭했을 때 동작 정의
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        // 버튼을 선택 후 음영이 사라지게 만들어 줌
        tableView.deselectRow(at: indexPath, animated: true)
        
        if indexPath.section == 0 && indexPath.row == 0 {
            
            let myIdVC = MyIDViewController(nibName: "MyIDViewController", bundle: nil)
            
            self.present(myIdVC, animated: true, completion: nil)
            
        } else if indexPath.section == 1 && indexPath.row == 0 {
            if let generalVC = UIStoryboard(name: "GeneralViewController", bundle: nil).instantiateViewController(identifier: "GeneralViewController") as? GeneralViewController {
                self.navigationController?.pushViewController(generalVC, animated: true)
            }
        }
    }
    
    // 현재 테이블에서 선택된 열(cell) 데이터
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        if indexPath.section == 0 {
            let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileViewCell", for: indexPath) as! ProfileViewCell
            
            // indexPath.section: 현재 세션 번호
            // indexPath.row: 현재 세션에서 열의 번호
            cell.topTitle.text = settingModel[indexPath.section][indexPath.row].menuIttile
            cell.profileImageView.image = UIImage(systemName: settingModel[indexPath.section][indexPath.row].leftImageName)
            cell.bottomDescription.text = settingModel[indexPath.section][indexPath.row].subTitle
            
            return cell
        }
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "MenuViewCell", for: indexPath) as! MenuViewCell
        
        cell.leftImageView.image = UIImage(systemName: settingModel[indexPath.section][indexPath.row].leftImageName)
        cell.leftImageView.tintColor = .red
        cell.middleTitle.text = settingModel[indexPath.section][indexPath.row].menuIttile
        cell.rightImageView.image = UIImage(systemName: settingModel[indexPath.section][indexPath.row].rightImageName ?? "")
        
        return cell
    }
    
    // 테이블의 셀 높이 지정
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        
        // 첫 번째 열은 해당 Cell의 높이 만큼으로 자동으로 설정해 준다.
        if indexPath.row == 0 {
            return UITableView.automaticDimension
        }
        
        // 나머지는 60
        return 60
    }
    
}

SettingModel.swift

import Foundation

struct SettingModel {
    var leftImageName: String = ""
    var menuIttile: String = ""
    var subTitle: String?
    var rightImageName: String?
}

ProfileViewCell

import UIKit

class ProfileViewCell: UITableViewCell {

    @IBOutlet weak var profileImageView: UIImageView!
    
    @IBOutlet weak var topTitle: UILabel!
    @IBOutlet weak var bottomDescription: UILabel!
    // nib의 최초 실행 코드
    override func awakeFromNib() {
        super.awakeFromNib()
        let profileImageHeight: CGFloat = 60
        profileImageView.layer.cornerRadius = profileImageHeight / 2
        topTitle.textColor = .blue
        topTitle.font = UIFont.systemFont(ofSize: 20)
        
        bottomDescription.textColor = .darkGray
        bottomDescription.font = UIFont.systemFont(ofSize: 16)
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
}

MenuViewCell

import UIKit

class MenuViewCell: UITableViewCell {

    @IBOutlet weak var leftImageView: UIImageView!
    @IBOutlet weak var middleTitle: UILabel!
    @IBOutlet weak var rightImageView: UIImageView!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }   
}

GeneralViewController & GeneralCell

  • Table View 정의
import UIKit


class GeneralCell: UITableViewCell {
    
    @IBOutlet weak var leftLabel: UILabel!
    
    @IBOutlet weak var rightImageView: UIImageView! {
        // 메모리에 올라가 인스턴스화가 될 때 호출된다
        // 즉 화면에서 만든 이미지와 코드가 연결되었을 때 호출
        didSet {
            rightImageView.image = UIImage.init(systemName: "chevron.right")
            rightImageView.backgroundColor = .clear
            rightImageView.tintColor = .orange
            
        }
    }
}

struct GeneralModel {
    var leftTitle = ""
}

class GeneralViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    var model = [[GeneralModel]]()
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // row count
        model[section].count
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return model.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "GeneralCell", for: indexPath) as! GeneralCell
        cell.leftLabel.text = model[indexPath.section][indexPath.row].leftTitle
        
        return cell
    }
    

    @IBOutlet weak var generalTableView: UITableView!
    

    
    // run only one
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.title = "General"
        
        generalTableView.delegate = self
        generalTableView.dataSource = self
        generalTableView.backgroundColor = UIColor(white: 245/255, alpha: 1)
        
        self.navigationController?.navigationBar.prefersLargeTitles = false
        
        model.append([GeneralModel(leftTitle: "About")])
        
        model.append(
            [GeneralModel(leftTitle: "KeyBoard"),
            GeneralModel(leftTitle: "Game Controller"),
            GeneralModel(leftTitle: "Fonts"),
            GeneralModel(leftTitle: "Language & Region"),
            GeneralModel(leftTitle: "Dictionary")])
        
        model.append([GeneralModel(leftTitle: "Dictionary")])
    }

}

MyIDViewController

  • 팝업 형태로 나오는 ViewController 정의
import UIKit

class MyIDViewController: UIViewController {
    
    // IBOutlet: 버튼의 색상 및 각종 설정을 할 때 사용하는 방식
    // @IBAction: 버튼이 눌렸을 때 행동을 정의
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var cancelButton: UIButton!
    @IBOutlet weak var nextButton: UIButton! {
        // 처음 셋팅할 때
        didSet {
            nextButton.isEnabled = false
        }
    }
    
    @IBAction func doCancel(_ sender: Any) {
        self.dismiss(animated: true)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // target: 실행해야할 function이 어디에 있는지 정의
        // editingChanged가 될 때마다 selector에 등록한 func에 메시지를 전달하겠다는 의미
        emailTextField.addTarget(self, action: #selector(textFieldDidChange), for: UIControl.Event.editingChanged)
    }
    
    @objc func textFieldDidChange(sender: UITextField) {
        if sender.text?.isEmpty == true {
            nextButton.isEnabled = false
        } else {
            nextButton.isEnabled = true
        }
    }
}

ViewController에 각각의 셀 뷰 등록

  • new file을 생성할 때 Subclass를 UITableViewCell로 생성
    image
  • 생성한 UITableViewCell의 ID 값을 설정
    image
  • TableView.register를 통해 생성한 Cell을 등록시킨다.
    // ViewController
    override func viewDidLoad() {
      super.viewDidLoad()
      settingTableView.delegate = self
      settingTableView.dataSource = self
      settingTableView.backgroundColor = UIColor(white: 245/255, alpha: 1)
      
      // ProfileViewCell에서 설정된 ID 값을 토대로 가지고 옴.
      settingTableView.register(UINib(nibName: "ProfileViewCell", bundle: nil), forCellReuseIdentifier: "ProfileViewCell")
      settingTableView.register(UINib(nibName: "MenuViewCell", bundle: nil), forCellReuseIdentifier: "MenuViewCell")
    }

Navigation Controller

image

데이터를 모델링 해서 setting의 열에 등록

Storyboard 이동(ViewController 전환)

Passing Data_1 ViewController 간 데이터 전달 방법(속성 값에 할당)

ViewController 속성 값에 할당

ViewController -> DetailViewController

// passing data (데이터를 넘겨주는 방법)
// 6가지

// 1. instance property

import UIKit

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

    @IBAction func moveToDetail(_ sender: Any) {
        let detailVC = DetailViewController(nibName: "DetailViewController", bundle: nil)
        detailVC.someString = "aaa 데이터"
        
        // 화면에 올라가기 전에 접근하면 에러가 발생
        // detailVC.someLabel.text = "bb"
        
        // present를 호출해야 화면에 올라갈 준비를 하기 때문에
        // present를 하기 전에 ui 요소에 접근을 하면 nil 에러가 발생
        self.present(detailVC, animated: true, completion: nil)
        
        // 보통 이런 방식으로 ui의 속성 값을 할당하지는 않는다.
        detailVC.someLabel.text = "bb"
        
    }
    
}
import UIKit

class DetailViewController: UIViewController {
    
    // ViewController instance가 생성되는 시점에
    // 해당 속성 값을 할당해 주고
    // 해당 값을 ui에 할당하는 시점은 viewDidLoad() 이후
    // 즉 ui가 메모리에 올라가고 나서 할당해 주는 방식을 사용한다.
    var someString = ""
    
    // instance가 생성되는 시점에는 nil이다.
    // 단, 화면에 UI들이 올라갈 수 있도록 준비 되는 시점에 ui가 메모리에 올라간다.
    // 즉, viewDidLoad가 될 때 생성된다.
    @IBOutlet weak var someLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // super.viewDidLoad()가 호출됐을 때 someLabel이 메모리에 올라가기 때문에 접근할 때 문제가 없다.
        someLabel.text = someString
    }

}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.