포스트

WidgetKit (4)

CleanShot 2024-12-04 at 18 20 52

이제 조금 더 확장을 해보려고한다.

RepoMediumView 만들기

CleanShot 2024-12-04 at 18 22 40

또 새로운 파일을 하나 만들어준다.

이때 이전에 언급하지는 않았지만

target을 항상 신경써서 만들자

이전에 NetworkManager의 경우 혹시 원 프로젝트 파일에서도 사용이 될 가능성이 있어서 둘다 체크를 했지만, 지금 View의 경우는 Widget에서만 사용 하므로, 굳이 할피룡가 없어 target을 하나만 한다.

혹시라도 하나만 체크하고 target을 추가하려면 해당 파일로가서

CleanShot 2024-12-04 at 18 29 14

이렇게 추가를 해주도록 하자.

그리고 widgetkit을 import해주는데 이렇게 되면

CleanShot 2024-12-04 at 18 30 41

프리뷰에서 위와 같은 에러가 발생한다.

아마 이전에 만들어졌던 코드를 사용하는듯 하다

1
2
3
4
5
6
struct Static_Widget_Previews: PreviewProvider {
    static var previews: some View {
        Static_WidgetEntryView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

새롭게 만든 뷰파일에 widget의 ui를 전부 옮겨주자.

내용은 패스

widget 크게에 따라 다르게 적용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct RepoWatcherWidgetEntryView : View {
    @Environment(\.widgetFamily) var family
    var entry: Provider.Entry
    
    var body: some View {
        switch family {
        case .systemMedium:
            RepoMediumView(repo: entry.repo)
        case .systemLarge:
            RepoMediumView(repo: entry.repo)
        case .systemSmall, .systemExtraLarge, .accessoryCircular, .accessoryRectangular, .accessoryInline:
            EmptyView()
        @unknown default:
            EmptyView()
        }
    }
}

환경변수를 만들어주고 해당 케이스에 맞게 적용을 해준다.

Repository 역할 분리 및 확장

기존방식의 문제

기존의 Repository 모델은 다음 두 가지 역할을 동시에 수행하고 있었다

  1. JSON 데이터를 디코딩하는 모델.
  2. 디코딩 후 Swift에서 사용하는 데이터 모델.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Repository: Decodable {
    let name: String
    let owner: Owner
    let hasIssues: Bool
    let forks: Int
    let watchers: Int
    let openIssues: Int
    let pushedAt: String
    
    static let placeholder = Repository(name: "Your Repo",
                                        owner: Owner(avatarUrl: ""),
                                        hasIssues: true, forks: 65,
                                        watchers: 123,
                                        openIssues: 55,
                                        pushedAt: "2024-11-04T05:22:15Z")
    

}

struct Owner: Decodable {
    let avatarUrl: String
}

// NetworkManager - 기존: JSON 데이터를 직접 디코딩
do {
    return try decoder.decode(Repository.self, from: data)
}

이와 같이 한 모델에 두 가지 역할이 합쳐지면 다음과 같은 문제가 생길 수 있다:

  • JSON 구조가 변경될 경우, Swift 모델의 구조까지 수정해야 하는 유지보수 문제가 발생.

CodingData 타입으로 분리

이를 해결하기 위해 JSON 디코딩과 Swift 모델 역할을 분리하여 관리하도록 변경했다.

참고글에 설명이 있으니 한번 읽어볼것.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
struct Repository {
    let name: String
    let owner: Owner
    let hasIssues: Bool
    let forks: Int
    let watchers: Int
    let openIssues: Int
    let pushedAt: String
    var avatarData: Data
    
    static let placeholder = Repository(name: "Your Repo",
                                        owner: Owner(avatarUrl: ""),
                                        hasIssues: true, forks: 65,
                                        watchers: 123,
                                        openIssues: 55,
                                        pushedAt: "2024-11-04T05:22:15Z",
                                        avatarData: Data())
    
}

struct Owner: Decodable {
    let avatarUrl: String
}

extension Repository {
    struct CodingData: Decodable {
        let name: String
        let owner: Owner
        let hasIssues: Bool
        let forks: Int
        let watchers: Int
        let openIssues: Int
        let pushedAt: String
        
        var repo: Repository {
            Repository(name: name,
                       owner: owner,
                       hasIssues: hasIssues,
                       forks: forks,
                       watchers: watchers,
                       openIssues: openIssues,
                       pushedAt: pushedAt,
                       avatarData: Data())
        }
    }

}

CodingData는 JSON 데이터를 디코딩하는 역할만을 담당한다. Repository와는 별도로 JSON 구조를 정확히 반영한다.

이걸 정리 해보면,

  1. CodingData 타입은 JSON을 디코딩한 뒤, 이를 기반으로 Repository 인스턴스를 생성한다.
    • CodingData는 JSON 구조를 정확히 반영하며, 디코딩된 데이터를 Repository로 변환하기 위한 repo 프로퍼티를 포함한다.
  2. Repository 타입은 Swift에서 주로 사용하며, JSON의 키 구조를 직접적으로 반영하지 않아도 된다.
    • JSON에서 데이터를 가져오고 Swift 모델로 변환하는 역할은 CodingData에서 처리한다.

그리고 NetworkManager에서도 바꿔주자.

1
2
3
4
5
// 변경 후: CodingData를 통해 변환
do {
    let codingData = try decoder.decode(Repository.CodingData.self, from: data)
    return codingData.repo
}

이렇게 하면 이미지를 처리할때도

Image(uiImage: UIImage(data: repo.avatarData) ?? UIImage(named: "avatar")!) 이렇게 repo에 바로 접근해서 가능하다.

원래라면 repo.owner.avatar로 접근했어야 했기 때문.

그리고 이미지만 Data()로 한 이유는

avatarData는 JSON 데이터에 포함되지 않기 때문이다.

  • avatarData는 Owner.avatarUrl을 사용해 네트워크에서 이미지를 다운로드한 후 추가로 저장되는 데이터이다.
  • JSON 디코딩 과정에서는 avatarData에 대한 값을 제공받지 않으므로, 기본값을 설정해야 디코딩 에러가 발생하지 않는다.

그래서 init할때만 빈 데이터를 넣고, 이후 필요한 값을 설정하는 방식으로 이루어진다.

이젠 Repository에 avatar에대한 정보가 있으니

1
2
3
4
struct RepoEntry: TimelineEntry {
    let date: Date
    let repo: Repository
}

여기에 있던 ImageData를 지워주도록 하자.

그러면 provider에서 ImageData가 없어졌으니 에러가 발생하니, imageData가 있던 부분을 지워주자.

사진은 생략

Timeline 부분은

1
2
3
4
5
6
7
8
do {
    var repo = try await NetworkManager.shared.getRepo(atUrl: RepoURL.google)
    let avatarImageData = await NetworkManager.shared.downloadImageData(from: repo.owner.avatarUrl)
    repo.avatarData = avatarImageData ?? Data()
    let entry = RepoEntry(date: .now, repo: repo)
    let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) // update every 12hours
    completion(timeline)
}

이렇게 처리해준다.

실행을 해보면

Dec-04-2024 19-23-04

이렇게 사이즈에따라 다르게 표시되는걸 알 수 있다.

Widget Size에 따라 달리 적용하기

지금은 위의 사진을 보면

하나의 repository만 있어서 large 사이즈의 경우 같은 레포지토리 정보가 중복으로 나오는 문제가 있다.

이유는

1
2
3
4
struct RepoEntry: TimelineEntry {
    let date: Date
    let repo: Repository
}

하나의 레포만 적용하기 때문.

이제 새롭게 레포를 만들어주는 옵셔널로 한다 왜냐? medium 사이즈에선 하나만 나와야하니까

MockData 만들어주기

기존에 Repository에 있던 값을 새로운 파일에 옮겨준다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct MockData {
    static let repoOne = Repository(name: "Repository 1",
                                    owner: Owner(avatarUrl: ""),
                                    hasIssues: true,
                                    forks: 65,
                                    watchers: 123,
                                    openIssues: 55,
                                    pushedAt: "2024-11-04T05:22:15Z",
                                    avatarData: Data())
    
    static let repoTwo = Repository(name: "Repository 2",
                                    owner: Owner(avatarUrl: ""),
                                    hasIssues: true,
                                    forks: 135,
                                    watchers: 253,
                                    openIssues: 245,
                                    pushedAt: "2024-01-04T05:22:15Z",
                                    avatarData: Data())
}

그리고 값이 바뀌었으니 적용을 하도록 하자. 적용하는 부분은 생략

Timeline 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
do {
    // Get Top Repo
    var repo = try await NetworkManager.shared.getRepo(atUrl: RepoURL.google)
    let avatarImageData = await NetworkManager.shared.downloadImageData(from: repo.owner.avatarUrl)
    repo.avatarData = avatarImageData ?? Data()
    
    // Get Bottom Repo if in Large Widget
    var bottomRepo: Repository?
    if context.family == .systemLarge {
        bottomRepo = try await NetworkManager.shared.getRepo(atUrl: RepoURL.swiftAlgorithms)
        let avatarImageData = await NetworkManager.shared.downloadImageData(from: bottomRepo!.owner.avatarUrl)
        bottomRepo!.avatarData = avatarImageData ?? Data()
    }
    
    // Create Entry & TimeLine
    let entry = RepoEntry(date: .now, repo: repo, bottomRepo: bottomRepo)
    let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) // update every 12hours
    completion(timeline)
}

크게 뭐 언급할 부분은 없을듯 하다.

View에 적용

1
2
3
4
5
6
7
case .systemLarge:
    VStack(spacing: 36) {
        RepoMediumView(repo: entry.repo)
        if let bottomRepo = entry.bottomRepo {
            RepoMediumView(repo: bottomRepo)
        }
    }

여기도 패스.

실행하면

simulator_screenshot_2FA64E85-3789-4506-B42C-5E18C00C82A4

아주 잘 나오는걸 볼 수 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.