[iOS] SceneDelegate에 대한 학습

SceneDelegate

AppDelegate & SceneDelegate

  • AppDelegate

iOS 13 미만

Process LifeCycle UI LifeCycle
App Launched Entered Foreground
App Terminate Became Active
... ...

iOS 13이상

AppDelegate SceneDelegate
Process LifeCycle UI LifeCycle
Scene LifeCycle Entered Foreground
Session Created Became Active
Session Discarded

iOS12까지는 하나의 앱은 하나의 window로 구성되어 있었다.
iOS13부터는 window의 개념이 scene으로 변경되었으며, 하나의 앱은 여러개의 scene을 가질수 있게 되었다.
다시 말해 하나의 앱에서 여러개의 scene을 보여줄수 있게 되었다.

SceneDelegate

AppDelegate의 역할 중 UI의 상태를 알 수 있는 UILifeCycle에 대한 부분을 SceneDelegate가 하게 되었다.

SceneDelegate의 생명주기

  1. [SceneDelegate] scene(_:, willConnectTo:, options:)

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    guard let _ = (scene as? UIWindowScene) else { return }
    }

    scene이 앱에 추가될 때 호출. 단 여기서 ViewController와 같은 클래스 객체를 만들어 사용할 때, 아직 viewDidLoad()가 호출되지 않음

  2. sceneDidDisconnect(_ 🙂

    func sceneDidDisconnect(_ scene: UIScene) {
    // Called as the scene is being released by the system.
    // This occurs shortly after the scene enters the background, or when its session is discarded.
    // Release any resources associated with this scene that can be re-created the next time the scene connects.
    // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    scene의 연결이 해제될 때 호출

  3. sceneDidBecomeActive(_: )

    func sceneDidBecomeActive(_ scene: UIScene) {
    // Called when the scene has moved from an inactive state to an active state.
    // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    app switcher에서 선택되는 등 scene과의 상호작용이 시작될 때 호출

    • app switcher : 홈버튼을 두번누르거나 아이폰 하단에서 위로 스와이프 했을 때 현재 실행중인 앱들이 보이는 화면
  4. sceneWillResignActive(_:)

    func sceneWillResignActive(_ scene: UIScene) {
    // Called when the scene will move from an active state to an inactive state.
    // This may occur due to temporary interruptions (ex. an incoming phone call).
    print("[SceneDelegate] sceneWillResignActive(_ scene: UIScene)")
    }

    사용자가 scene과의 상호작용을 중지할 때 호출(다른 화면으로의 전환과 같은 경우)

  5. sceneWillEnterForeground(_:)

    func sceneWillEnterForeground(_ scene: UIScene) {
    // Called as the scene transitions from the background to the foreground.
    // Use this method to undo the changes made on entering the background.
    print("[SceneDelegate] sceneWillEnterForeground(_ scene: UIScene)")
    }

    scene이 foreground로 진입할 때 호출

  6. sceneDidEnterBackground(_:)

    func sceneDidEnterBackground(_ scene: UIScene) {
    // Called as the scene transitions from the foreground to the background.
    // Use this method to save data, release shared resources, and store enough scene-specific state information
    // to restore the scene back to its current state.
    print("[SceneDelegate] sceneDidEnterBackground(_ scene: UIScene)")
    }

    scene이 background로 진입할 때 호출

AppDelegate 생명주기

AppDelegate - Scene LifeCycle

AppDelegate에는 Session LifeCycle에 대한 역할이 추가되었다.
SceneSession은 앱에서 생성한 모든 scene의 정보를 관리한다.

// 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.
}

Scene?

UIKit은 UIWindowScene 객체를 사용하는 앱 UI의 각 인스턴스를 관리한다. Scene에는 UI의 하나의 인스턴스를 나타내는 windows와 view controllers가 들어있다. 또한, 각 scene에 해당하는 UIWindowSceneDelegate 객체를 가지고 있고, 이 객체는 UIKit과 앱 간의 상호작용을 조정하는데 사용된다.
Scene들은 같은 메모리와 앱 프로세스 공간을 공유하면서 서로 동시에 실행된다. 결과적으로 하나의 앱은 여러 Scene과 Scene delegate 객체를 동시에 활성화 할 수 있다.
(Scenes - Apple Development Document)

Scene Session?

UISceneSession 객체는 Scene의 고유의 런타임 인스턴스를 관리한다. 사용자가 앱에 새로운 Scene을 추가하거나 요청하면, 시스템은 그 Scene을 추적하는 session 객체를 생성한다. 그 Session에는 고유한 식별자와 scene의 구성 세부사항(configuration details)가 들어있다.
UIKit은 session 정보를 그 Scene 자체의 생명주기동안 유지하고 app switcher에서 사용자가 그 Scene을 클로징하는 것에 대응하여 그 session을 파괴한다.
session 객체는 직접 생성하지 않고 UIKit이 앱의 사용자 인터페이스에 대응하여 생성한다.
또한, 아래 함수를 통해서 UIKit에 새로운 Scene과 Session을 프로그래밍적 방식으로 생성할 수 있다.
(UISceneSession - Apple Development Document)

func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration 
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)

iOS13부터 AppDelegate의 역할

  1. 앱의 데이터 구조를 초기화 하는 것
  2. 앱의 Scenes의 환경설정(Configuration)
  3. 앱 밖에서발생한 알림(베터리 부족, 다운로드 완료 등)에 대응
  4. 특정한 Scenes, views, view controllers에 한정되지 않고 앱 자체를 타겟하는 이벤트에 대응하는 것
  5. 애플 푸쉬 알림 서브스와 같이 실행시 요구되는 모든 서비스를 등록
    (UIApplicationDelegate - Apple Development Document)

Deployment Target이 iOS 13 미만인 상황에서는?

iOS12이하는 단일 window 환경임으로, 아래와 같이 Scene처리를 제거할 수 있다.

  1. SceneDelegate.swift 제거
  2. AppDelegate에서 UISceneSession 관련 두개의 메소드 제거
  3. SceneDelegate로 옮겨진 window 프로퍼티를 AppDelegate로 옮기기
    var window: UIWindow?
  4. info.plist에서 Scene과 관련된 Manifest인 Application Scene Manifest 삭제

GRDB를 사용하다가 생긴 문제점 공유

GRDB를 사용하다보니 JOIN시에 해당 구조체를 가져오는게 쉽지 않다는 문제점을 발견했다.
이를 해결하기 위해, 검색을 하다보니 GRDB를 더 깊게 이해해야 한다는 문제점에 도달했다.

Sqlite3으로 모든 작업을 돌리기로 결정.
이유는 Sqlite3은 내가 맘대로 만들고, 제어도 더 쉽다고 생각했다.
Join과 cascade등의 처리를 위해서는 Sqlite3을 직접 사용하는게 더 좋은것 같다.

일단, Sqltie3으로 개발을 완료하고 나서 추후 nosql을 사용해보겠다.

오랫만에 개인 프로젝트를 하나 해보고 있는데, 트랜드를 다시 익히려다 보니 온갖 삽질을 다 하고 있다..

GRDB를 사용해서 대부분의 것들도 사용이 가능한거 같긴한데, 사전 지식이 꽤 많이 필요한거 같다.

RxSwift 쉽지 않구나.

Swift를 지나고 Reactive를 지나서 가는 여정이 참으로 험난하다.
뭔가 깨달은것 같으면 다시 막힘에 들어서는 반복중이다.

간단한걸 RxSwift MVVM으로 만드는게 이렇게 고통스러울줄이야...
좀 자료를 정리해서 글을 남겨야 할것 같다.

어떻게 글을 정리하고 글을 써야 할지 고민을 좀 해봐야 겠다.
정리가 안된다.. ㅠ

RxTableView

RxSwift를 학습은 했는데, 쓸 때마다 기억이 안난다.
아직 익숙해지지 않아서일까.. 그래서 정리해본다.

어떻게 정리해봐야 할까 고민을 해보아야 할것 같다.
일단 방식만 나열하는 걸로...

총 네가지 방식의 사용법이 존재한다.

1. tableView.rx.items 사용하기

    func bindingTableViewItems01() {
        let cities = ["01", "L", "K"]
        let citiesOb: Observable<[String]> = Observable.of(cities)
        citiesOb.bind(to: tableView.rx.items) { (tableView: UITableView, index: Int, element: String) -> UITableViewCell in
            guard let cell = tableView.dequeueReusableCell(withIdentifier: TitleCell.identifier) as? TitleCell else {
                return UITableViewCell()
            }
            cell.title?.text = element
            
            // CellType 변경
            // element 타입을 기준으로 셀을 리턴 가능

            return cell
        }
        .disposed(by: disposeBag)
    }

2. tableView.rx.items(cellIdentifier:String) 사용하기


    func bindingTableViewItems02() {
        let cities = ["02", "L", "K"]
        let citiesOb: Observable<[String]> = Observable.of(cities)
        citiesOb.bind(to: tableView.rx.items(cellIdentifier: TitleCell.identifier))
        { (index: Int, element: String, cell: UITableViewCell) in
            if let cell = cell as? TitleCell  {
                cell.title.text = element
            }
        }
        .disposed(by: disposeBag)
    }
   

3. tableView.rx.items(cellIdendifier:String,cellType:Cell.Type) 사용하기


    func bindingTableViewItems03() {
        let cities = ["03", "L", "K"]
        let citiesOb: Observable<[String]> = Observable.of(cities)
        citiesOb.bind(to: tableView.rx.items(cellIdentifier: TitleCell.identifier, cellType: TitleCell.self))
        { (index: Int, element: String, cell: TitleCell) in
            cell.title.text = element
            cell.textLabel?.text = element + "     " + element
        }
        .disposed(by: disposeBag)
    }

4. tableView.rx.items(dataSource:protocol<RxTableViewDataSourceType, UITabelViewDataSource>) 사용하기


    // tableView를 어떻게 표현할지 미리 지정한 datasource를 사용한 방법
    // pod으로 RxDataSource를 설치
    // TODO: 참고 자료 https://github.com/RxSwiftCommunity/RxDataSources
    func bindingTableViewItems04() {
        // RxDataSource에서는 SectionModelType을 따르는 SectionModel을 이미 구현해 놓았는데, 이것을 사용하면 된다.
        typealias CitySectionModel = SectionModel<String, String>
        typealias CityDataSource = RxTableViewSectionedReloadDataSource<CitySectionModel>
        
        let cities = ["03", "L", "K", "L", "K", "L", "K", "L", "K", "L", "K"]
        let sections = [
            CitySectionModel(model: "first section", items: cities),
            CitySectionModel(model: "second section", items: cities)
        ]
        
        let configureCell: (TableViewSectionedDataSource<CitySectionModel>, UITableView, IndexPath, String) -> UITableViewCell = {
            (datasource, tableView, indexPath, element) in
            guard let cell = tableView.dequeueReusableCell(withIdentifier: TitleCell.identifier, for: indexPath) as? TitleCell else {
                return UITableViewCell()
            }
            cell.title.text = element
            return cell
        }
        let datasource = CityDataSource.init(configureCell: configureCell)

        datasource.titleForHeaderInSection = { datasource, index in
            return datasource.sectionModels[index].model
        }
                
        Observable.just(sections)
            .bind(to: tableView.rx.items(dataSource: datasource))
            .disposed(by: disposeBag)
    }

@escaping

다음과 같은 블럭함수를 사용할 경우에, 에러가 발생한다.
"Escaping closure captures non-escaping parameter 'completion'"

    func testFunc(_ completion: (String?) -> Void ) {
        DispatchQueue.global().async {
            print("global something...")
            
            DispatchQueue.main.async {
                completion("End")
            }
        }
    }

원인은 testFunc이 종료되고 나서 completion이 호출되는데, 호출이 불가하기 때문이다.
이러한 경우에 @escaping 을 선언해주면 정상 동작을 한다.

    func testFunc(_ completion: @escaping (String?) -> Void ) {
        DispatchQueue.global().async {
            print("global something...")
            
            DispatchQueue.main.async {
                completion("End")
            }
        }
    }

여기까지는 모두 알고 있는 항목이였을 것이다.
굳이 뭐 이런 글을이라며....
하지만, 오늘 배운게 하나 있는데, completion이 옵셔널로 선언된다면, default값으로 @escaping이 설정된다는 것이였다.


다음과 같이 코드 작성이 가능하다는 말이다.

    func testFunc(_ completion: ((String?) -> Void)? ) {
        DispatchQueue.global().async {
            print("global something...")
            
            DispatchQueue.main.async {
                completion?("End")
            }
        }
    }

난 분명 까먹을것이기 때문에, 기록을 남긴다.

protocol Equatable

글 작성중입니다.
글 하나 작성하는데 상당히 오랜 시간이 필요하네요..
곧 마무리 해서 올리겠습니다.

iOS 개발을 하다보면 꽤 많은 프로토콜들이 등장한다.
대부분의 개발자들은 별 생각없이 넘어갈것이라고 생각한다.
그러다가 어느순간에 이건 도대체 뭐지? 라는 의문이 들것이고 이에 대해 알고 싶어질것이라 생각한다.
지금 내가 그렇고, 매번 궁금한게 생길때마다 다시 찾아보는 수고를 덜기 위해 정리해보려고 한다.

equatable| ikwéitəbl | 형용사 | 동일시할 수 있는.

아래 애플링크에 있는 글들을 정리를 좀 해볼께요.
좀 보기 편하게 제 스타일대로 의역을 좀 해놨습니다.
틀린부분이 있다면 알려주세요.

Equatable

Type의 같음을 비교할 수 있다.

개요

Equatable 프로토콜을 따르는 Type들은 equal-to operator (==) 또는 불균형인지를 not-equal-to operator (!=)를 사용하여 비교 할수 있다.

배열와 컬렉션은 Equatable을 만족할때, 쉽게 비교를 수행할수 있다.
예를 들면 다음과 같다.
배열의 요소들이 Equatable을 만족할 때, 제공되는 closure를 사용하여 비교하지 않고, 특별한 값을 포함하고 있는지를 contains(_:) 메소드를 사용하여 체크할수 있다.

let students = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"]

let nameToCheck = "Kofi"
if students.contains(nameToCheck) {
    print("\(nameToCheck) is signed up!")
} else {
    print("No record of \(nameToCheck).")
}
// Prints "Kofi is signed up!"

Equatable Protocol을 따르기

Equatable을 당신이 만든 Type에 추가한다는 것은, Collection에서 특별한 인스턴스를 찾기 위해 convenient APIs를 사용한다는 뜻이다.
Equatable은 base protocol이며, Hashable과 Comparable protocols에도 사용된다.
그리고, Collection의 요소들을 sets과 sorting하여 구성하는 것처럼, custom type을 사용할수 있게 해준다.

Equtable을 만족시며 type를 생성하기 위해서는 다음 두가지를 충족시켜야 한다.

  • Struct일 경우, 모든 properties가 Equatable을 만족 시켜야 한다.
  • Enum일 경우, 모든 value들이 Equatable을 만족 시켜야 한다.
    (value없는 enum은 선언없이 만족된다. 맞는 해석인지 모르겠다. 테스트 필요.)

작성중...

  • 참고 링크
  • https://developer.apple.com/documentation/swift/equatable

억음부호 `Swift`

나이가 들어가면서, 점점 미래가 불안해지고 뭐하고 먹고 살아가야하나 고민을 하고 있다.

그래서, 내가 제일 잘 할수 있는 코딩(?)을 더 잘하려고 한다.
늦은감이 있지만, RxSwift부터 최신트랜드를 학습하며 따라가려고 노력중이다.
Swift를 좀 깊게 학습하고 싶어서 코쟁이 형들하고, 잘한다는 형들의 소스를 보고 있는 중이다.

그러던 오늘, 억음이라고 불리는 ` 표시의 소스코드를 발견하였다.
이 표시가 억음이라는걸 알아내는데도 꽤 시간이 걸렸다. 영어로는 backtick이라고 표현하는것 같다.(추측..)

Martin이라는 멋진형이 쓴글을 직독해서 블로그한다.
마틴형 링크는 하단에 표기해놓았다. 똑똑한 형 같으니 그 블로그를 뒤적거려보는것도 좋을것 같다.
오역이 있을수 있으니, 오역이나 더 좋은 표현이 있다면 댓글로 남겨주길 부탁한다. (반말로 쓰는 이유는 내 블로그에 내가 쓰면서 존칭을 쓰면 이상한 기분이 들어서랄까...)

`은 language keywords로부터 identifier를 만들수 있게 해준다고 한다.

Swift에서 C 또는 Objective-C function을 직접 호출할수 있다, 그러나 C function이름이 guard라면 억음 없이 호출할수 없다.

`guard`()

더 좋은 예제로 enum을 들수 있다. 다음과 같은 값들을 가진 enum을 만들고 싶을수 있다.

enum Foo {
    case `var`
    case `let`
    case `class`
    case `func`
}

let items = [Foo.class, Foo.let, .var]

억음을 사용하면 reserved language keywords를 사용할 수 있다.

당신이 애플 API 디자인처럼 default keyword를 property 이름으로 사용하고 싶다면, 이때 억음을 사용할 수 있다.
NotificationCenter가 싱글톤으로 default를 사용하고 있으며, 이 default는 keyword이며 switch statements로 사용된다.
이를 구현하기 위해서는 다음과 같이 사용하면 된다.

class MyOwnNotificationCenter {
    static let `default` = MyOwnNotificationCenter()
    ...
}

클로져에서 [weak self]를 사용해본적이 있나요? 그렇지 않다면 사용하셔야 합니다.
self를 weak reference하게 만들었을때, self는 optional이고 가끔씩은 문제를 일으키게 됩니다.
Swift4.2 이전에 strong reference self의 이름을 유지하기 위해 다음과 같이 사용했습니다. (또는 strongSelf로 이름을 바꾸어서 사용)

self.provider.fetchData { [weak self] (fetchedData) in
    guard let `self` = self else { return }
    let processedData = self.processData(fetchedData)
    self.updateUI(with: processedData)
}

Swift4.2에서는 다음과 같이 사용합니다.

self.provider.fetchData { [weak self] (fetchedData) in
    guard let self = self else { return }
    let processedData = self.processData(fetchedData)
    self.updateUI(with: processedData)
}

이제 optional binding을 사용해서 weak를 strong으로 reference를 업그레이드 하였습니다.

  • 참고 사이트
  • https://blog.entirely.digital/swift-backticks/