천원의 개발

iOS Realm 데이터베이스를 활용하여 위젯 만들기(Realm 파일 공유) 본문

iOS&Swift🍎/iOS

iOS Realm 데이터베이스를 활용하여 위젯 만들기(Realm 파일 공유)

천 원 2023. 1. 2. 11:01

먼저 본격적으로 들어가기 전에 어떤 위젯이 좋은 위젯일까요?
WWDC에서는 personalized하고 glanceable한 위젯이 좋은 위젯이라고 하더라구요
그럼 이제 하나씩 어떤 뜻인지 살펴보겠습니다.

먼저 Personalized한 위젯이란? 간단한 날씨 위젯을 예제로 살펴보면

각 개인이 사는 지역마다 자신의 지역의 날씨 정보를 알려줘야겠죠. 이렇게 개인에 따라 맞춤 정보를 제공하는 것을 Personalized라고 합니다.

 

 

다음으로 glanceable한 앱들의 예제를 확인해 보시면

우리가 어떠한 동작을 하지 않아도 바로 간편하게 원하는 정보를 확인할 수 있죠!
이런 부분을 glanceable하다고 합니다.

 

 

또한 WWDC에서 강조한 부분이 바로 Widget are not mini-apps!!! 많은 분들이 착각하시는게 위젯은 작은 앱이 아닙니다.

위젯은 사용자가 필요한 정보를 볼 수 있게 해주는 전광판 같은 역할이지 작은 앱이 아닙니다!!!

버튼을 못 넣어요!!! 그런데 네이버나 토스 위젯을 보시면

 

버튼이 있지 않나? 생각하실 수 있지만 실질적으로 위젯 내부에서 검색 혹은 송금을 하는 것이 아닌 Link를 통해서 앱의 원하는 화면을 띄워주게 하는 것이지 위젯이 앱처럼 동작을 할 수 있는 것은 아닙니다.

참고로 위젯의 메모리 제한이 30mb밖에 안 되기 때문에 만드실 때 주의해주셔야 합니다.

 

 

이제 본격적으로 앱을 만들어 보겠습니다. 

new에서 target을 추가한 후 widget을 검색하여 추가 해줍니다.


그런데 여기서 target이 어떤것일까요? 이부분은 뒤에서 다루겠습니다.

 

아래 같은 화면이 나오는데 보시면 두가지 옵션을 설정할 수 있어요!

먼저 Include Live Activity 옵션은 이번에 새로나온 다이나믹 아이랜드를 만들 수 있는 설정입니다. 다이나믹 아일랜드 관심이 있으시면 공부해보시면 좋을거 같아요.

 

 

 

다음으로 Configuration intent인데 이제 이 항목을 체크하면 IntentConfiguration위젯을 만들 수 있습니다. 

IntentConfiguration위젯은 기본 달력 위젯처럼 개인이 설정할 수 있는 항목을 가진 위젯입니다. 반대로 StaticConfiguration 위젯은 설정 항목이 없는 위젯을 말합니다.

 

 

 

 


저는 Product Name만 추가해 주고 두항목은 해제해서 StaticConfiguration 위젯을 만들어 보겠습니다.

 

 

 

 

 

그러면 코드가 많이 작성되어 있는데 겁먹지 마시고 하나하나 같이 살펴보겠습니다.

 

 

TestWidet 구조체를 보시면 Widget 프로토콜을 채택하고 있습니다.

이제 Widget프로토콜의 설명을 보시면 홈 스크린 또는 알림 센터에 표시할 위젯의 구성 및 내용이라고 정의되어 있습니다.

말그대로 위젯을 구성하는 곳입니다.

보시면 방금전에 설명한 StaticConfiguration을 사용해서 만들건데 매개변서로 kindprovider 그리고 closer를 가지고 있습니다.

kind는 위젯을 식별하기 위한 아이디라고 생각하시면 되고 provider는 위젯을 새로고침할 타임라인을 결청하는 객체입니다.

마지막으로 클로저 안에는 위젯을 렌더링하기 위한 SwfitUI View가 포함되어있습니다.  <- 이게 무슨 소리인가 하면 testWidgetEntryView를 한번 살펴보겠습니다.

 

 

 

SwiftUI 문법을 통해서 View를 그리고 있는 모습입니다. Vstack과 text를 통해서 SeSAC글자를 추가해준 모습입니다.

 

 

 

 

이제 설정들을 확인해 보면 configurationDisplayname을 통해서 위젯 추가 화면에서 보여질 이름을 변경할 수 있습니다.

description을 통해서 설명이 가능하고 supportedFamilies를 통해서 지원할 위젯의 크기를 정할 수 있습니다.

 

 

이제 provider 매개변수를 한번 살펴보러 가면

 

 

TimelineProvider 프로토콜을 채택하고 있는 모습입니다. TimelineProvider 포로토콜은 위젯의 화면 표시를 업데이트할 시기를

Widgetkit에 알려주는 프로토콜입니다. 그러면 여기서 위젯의 업데이트 동작 방식에 대해서 한번 알아보겠습니다.

 

 

 

 

우리가 업데이트시키고 싶은 시간을 위젯에 미리 알려주어서 위젯이 미리 그 시간의 화면을 구성해 두었다가 변경하는 방식으로 동작합니다. 예제를 보시면 9시 -> 9시 30분 -> 10시 5분에 위젯을 업데이트해달라고 요청하면 위젯은 미리 뷰를 만들어 두었다가 그 시간에 맞추어서  업데이트해 주는 방식입니다.

 

 

 

기본적으로 작성되어있는 코드를 보시면 entries배열에 반복문을 통해서 현재시간의 1시간뒤, 2시간뒤 총 5시간 뒤까지 시간을 담는 반복문이 작성되어 있습니다.

그렇다면 현재 시간이 6시라고 한다면 7시, 8시, 9시, 10시, 11시 이렇게 총 5개의 시간을 담아주는 겁니다. 이제 담아둔 시간을 timeline 실어서  completion으로 보내줍니다. 그러면 이제 담아둔 시간들 마다 위젯이 업데이트가 될겁니다!! 

그런데 만약에 우리가 담아둔 마지막 시간인 11시 이후에는 이제 업데이트할 시간이 없는데 어떻게 될까요?

이제 그 부분은 여기 policy를 통해서 설정해 줄 수 있습니다.

 

 

 

 

timelineReloadPolicy에는 총 3개가 있는데 atEnd는 타임라인의 마지막 시간이 지난 후 그러니까 우리 입장에서는 11시가 지난 후 Widgetkit이 새 태임라인을 요청하도록 합니다. 그러면 이제 11시를 기준으로 다시 총 1시간씩 5개가 생성이 되는거죠

after는 atEnd랑 비슷한데 새로 요청할 시간의 간격을 설정할 수 있습니다. 만약에 date에 2시간을 넣는다면 11시에서 2시간이 지난 13시부터 총 1시간씩 5개가 생성이 되는겁니다.

마지막으로 never는 11시 이후로 더 이상 갱신을 안 하겠다고 설정하는 것입니다.

 

 

 

그런데 애플은 우리가 정확히 갱신 시기를 잡아둬도 업데이트가 그 시간에 정확히 되지는 않는다고 합니다.
또한 하루에 40-70번 정도의 갱신만 가능하다고 합니다.

 

 

그렇다면... 일정관리 위젯처럼 새로운 사항이 추가되거나 삭제되는 앱은 어떻게 관리할까요?

 

바로 바로 WidgetCenter를 활용하는 것입니다.

 

WidgetCenter의 realoadAllTimelines 메서드를 호출하여 필요한 시점에서 위젯의 갱신이 가능합니다.

  WidgetCenter.shared.reloadAllTimelines()

 

 

 

 

이제 본격적으로 한번 만들어 보겠습니다.

아이즈원 멤버의 이름과 나이를 보여주는 위젯을 만들어 보겠습니다.

 

 

가장 먼저 Realm을 impoart 해줍니다.

그러면 바로 컴파일 에러가 발생하는데 그 이유가 바로 target의 개념 때문입니다.

 

보시면 위젯은 기존의 앱과는 다른 target으로 존재하죠! 그렇다면 target의 개념부터 한번 살펴보겠습니다.

 

 

 

공식문서 해석:

Target이란 빌드할 프로덕트를 정의하고 프로젝트 or 워크스페이스의 파일로부터 빌드되는 프로덕트에 지시들을 포함합니다. 또한 Target 하나의 프로덕트를 정의하고 있습니다. 이러한 지시들에는소스파일, 어떤 소스파일을 처리할지 등의 정보들을 포함하고 있습니다.

 

음.. 해석을 보셔도 이해가 잘 안되시죠! 제가 3줄 요약 해드리겠습니다.

 

- 하나의 Target은 하나의 프로덕트(앱)이다.

- 한 프로젝트는 여러개의 Target으로 이루어 질 수 있다.

- Target별로 빌드 설정을 달리할 수 있다.

 

하나의 Target이 하나의 앱이니까 Widget또한 개인적으로 빌드가 가능합니다.

이제 우리는 빨간 줄을 없애기 위해서 중요하게 봐야하는 점이 Target별로 빌드 설정을 달리 할 수 있다는 점입니다.

 

 

앱의 Build Phases를 확인해 보면 라이브러리에 정상적으로 Realm이 추가되어있는 모습을 확인할 수 있습니다.

 

반면에 위젯의 Build Phases를 확인해 보면 Realm이 없는것을 확인할 수 있습니다.

 

그래서 Realm을 추가해 주면 정상적으로 Realm의 import가 가능합니다.

 

 

이제 다음으로 Realm 데이터를 가져오기 위해서 싱글턴 형태의 RealmManager를 만들어서 아이즈원의 데이터를 getData메서드를 통해서 한번 가져와 보겠습니다.

 

 

 

 

 

위젯을 구성하는 TestWidgetEntryView에서 RealmManager를 호출하면 다시 한번 런타임 오류를 만나게 됩니다.

역시 이번에도 Target 별로 빌드 설정이 달라서 일어난 오류인데

 

 

 

 

RealmManager 파일의 Targetmembership을 보시면 앱에만 체크가 되어있죠?

그래서 위젯을 빌드시에는 사용이 불가능하기 때문에 컴파일 오류가 발생한 것입니다. 그래서 Target Membership에 위젯도 체크 해주고  

 

 

 

 

아이즈원 테이블도 Target Membership을 설정해 주면 이제 정상적으로 RealmManager를 호출할 수 있습니다.

이제 받아온 데이터의 첫번째 인덱스에 name을 Text로 보여주면.. 안나옵니다.!!!

왜 안나오나? 하면 

 

아래 사진을 보시면 App과 AppExtension은 각각 다른 container를 가지고 있기 때문에 우리는 App container안에 있는 Realm 파일을 가져와서 shared Container로 옮겨준 후 위젯에서 읽어야 합니다.

 

 

자 이제 우리는 AppGroup을 통해서 Shared Container를 만들어 줄겁니다.

 

위젯과 app target에 각각 AppGroup을 추가해준 모습입니다.

 

 

그런 다음에 기존의 Realm 파일을 AppDelegate에서 shared Container로 Migration 해주는 코드를 작성합니다.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let defaultRealm = Realm.Configuration.defaultConfiguration.fileURL!
    let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.sesac.WidegtSample")
    let realmURL = container?.appendingPathComponent("default.realm")
    var config: Realm.Configuration!

    // Checking the old realm config is exist
    if FileManager.default.fileExists(atPath: defaultRealm.path) {
        do {
            _ = try FileManager.default.replaceItemAt(realmURL!, withItemAt: defaultRealm)
           config = Realm.Configuration(fileURL: realmURL, schemaVersion: 1)
        } catch {
           print("Error info: \(error)")
        }
    } else {
         config = Realm.Configuration(fileURL: realmURL, schemaVersion: 1)
    }

    Realm.Configuration.defaultConfiguration = config

    return true
}

 

위젯의 RealmManager 에서도 데이터를 불러 올때 Realm 경로 설정을 Shared Container로 설정해주면!!

import UIKit
import RealmSwift

class RealmManager {
    
    private init() { }
    
    static let shared: RealmManager = .init()
     
    private var localRealm: Realm {
        let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.sesac.WidegtSample")
        let realmURL = container?.appendingPathComponent("default.realm")
        let config = Realm.Configuration(fileURL: realmURL, schemaVersion: 1)
        return try! Realm(configuration: config)
    }
    
    
    func getData() -> [Izone] {
        Array(localRealm.objects(Izone.self))
    }

    
}

 

🎉 따란~ 정상적으로 Realm데이터를 출력하는 모습입니다!!

 

 

여기까지 Realm 데이터베이스를 적용하여 위젯 만들기 였습니다.

 

 

전체코드 Git

https://github.com/Yoon-hub/WidgetSample

 

GitHub - Yoon-hub/WidgetSample

Contribute to Yoon-hub/WidgetSample development by creating an account on GitHub.

github.com