관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - SwiftUI] 4. 위젯 Widget 사용 방법 - 위젯 이미지 로드 방법 본문

iOS 응용 (SwiftUI)

[iOS - SwiftUI] 4. 위젯 Widget 사용 방법 - 위젯 이미지 로드 방법

jake-kim 2022. 10. 4. 22:53

1. 위젯 Widget 사용 방법 - WidgetKit, WidgetFamily

2. 위젯 Widget 사용 방법 - API 데이터 로드와 위젯UI 업데이트

3. 위젯 Widget 사용 방법 - 위젯 딥링크 구현 방법 (widgetURL)

4. 위젯 Widget 사용 방법 - 위젯 이미지 로드 방법

5. 위젯 Widget 사용 방법 - Provisioning Profile 등록 (WidgetExtension)

이미지를 보여주는 위젯

위젯에 사진 로드 방법

  • 이미지가 아닌 데이터는 상관 없지만, 위젯에서는 이미지를 async하게 로딩하는것을 지원하지 않으므로 sync하게 수행해야함을 주의
  • 이미지 캐시를 사용하여, 딥링크 처리하는 쪽에서도 쉽게 처리되도록 구현

구현

  • 예제 프로젝트 생성 -> 1번 포스팅 글에서 알아본 대로 WidgetKit Extension 추가

  • 이미지를 캐싱하는 ImageCache.swift 파일 추가 (Targets에 MyWidgetExtension도 추가)

  • ImageCache 구현
    • 싱글톤으로 접근가능하도록 구현
import Foundation
import UIKit

final class ImageCache {
  let shared = NSCache<NSString, UIImage>()
  
  private init() {}
}
  • Widget 예제 코드 준비
https://api.flickr.com/services/feeds/photos_public.gne?tags=texas&tagmode=any&format=json&nojsoncallback=1

 

  • 응답형식
{
		"title": "Recent Uploads tagged texas",
		"link": "https:\/\/www.flickr.com\/photos\/tags\/texas\/",
		"description": "",
		"modified": "2022-09-25T04:53:35Z",
		"generator": "https:\/\/www.flickr.com",
		"items": [
	   {
			"title": "DSC_0239-10",
			"link": "https:\/\/www.flickr.com\/photos\/jennsunique\/52381172512\/",
			"media": {"m":"https:\/\/live.staticflickr.com\/65535\/52381172512_2269e63e8c_m.jpg"},
			"date_taken": "2022-09-25T00:52:03-08:00",
			"description": " <p><a href=\"https:\/\/www.flickr.com\/people\/jennsunique\/\">jennsunique<\/a> posted a photo:<\/p> <p><a href=\"https:\/\/www.flickr.com\/photos\/jennsunique\/52381172512\/\" title=\"DSC_0239-10\"><img src=\"https:\/\/live.staticflickr.com\/65535\/52381172512_2269e63e8c_m.jpg\" width=\"240\" height=\"160\" alt=\"DSC_0239-10\" \/><\/a><\/p> <p>Mayer Park, Spring, Texas<\/p>",
			"published": "2022-09-25T04:53:35Z",
			"author": "nobody@flickr.com (\"jennsunique\")",
			"author_id": "23580355@N06",
			"tags": "mayerpark spring texas nature trail cypresscreek creek walk park outdoors outside path jennifermcfarland"
	   },
	   {
			"title": "DSC_0097-10",
			"link": "https:\/\/www.flickr.com\/photos\/jennsunique\/52381173057\/",
			"media": {"m":"https:\/\/live.staticflickr.com\/65535\/52381173057_f6bcb1fc83_m.jpg"},
			"date_taken": "2022-09-25T00:52:24-08:00",
			"description": " <p><a href=\"https:\/\/www.flickr.com\/people\/jennsunique\/\">jennsunique<\/a> posted a photo:<\/p> <p><a href=\"https:\/\/www.flickr.com\/photos\/jennsunique\/52381173057\/\" title=\"DSC_0097-10\"><img src=\"https:\/\/live.staticflickr.com\/65535\/52381173057_f6bcb1fc83_m.jpg\" width=\"240\" height=\"160\" alt=\"DSC_0097-10\" \/><\/a><\/p> <p>Mayer Park, Spring, Texas<\/p>",
			"published": "2022-09-25T04:53:26Z",
			"author": "nobody@flickr.com (\"jennsunique\")",
			"author_id": "23580355@N06",
			"tags": "mayerpark spring texas nature trail cypresscreek creek walk park outdoors outside path jennifermcfarland"
	   },
       
       ...
  • Codable Model 정의
struct PhotoModel: Codable {
  struct Item: Codable {
    struct Media: Codable {
      let m: String
    }
    let media: Media
  }
  let items: [Item]
}
extension PhotoModel {
  var url: String? {
    items.first?.media.m
  }
}
// MyWidget.swift

import WidgetKit
import SwiftUI
import Intents

struct Provider: IntentTimelineProvider {
  func placeholder(in context: Context) -> SimpleEntry {
    SimpleEntry(date: Date(), uiImage: UIImage(), url: "")
  }
  
  func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    getPhoto { uiImage, url in
      let entry = SimpleEntry(date: Date(), uiImage: uiImage, url: url)
      completion(entry)
    }
  }
  
  func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    getPhoto { uiImage, url in
      let currentDate = Date()
      let entry = SimpleEntry(date: currentDate, uiImage: uiImage, url: url)
      let nextRefresh = Calendar.current.date(byAdding: .minute, value: 3, to: currentDate)!
      let timeline = Timeline(entries: [entry], policy: .after(nextRefresh))
      completion(timeline)
    }
  }
  
  private func getPhoto(completion: @escaping (UIImage, String) -> ()) {
    // TODO
  }
}

struct SimpleEntry: TimelineEntry {
  let date: Date
  let uiImage: UIImage
  let url: String
}

struct MyWidgetEntryView : View {
  var entry: Provider.Entry
  
  var body: some View {
    Image(uiImage: entry.uiImage)
      .resizable()
      .aspectRatio(contentMode: .fill)
      .widgetURL(URL(string: getPercentEcododedString("widget://deeplink?url=\(entry.url)")))
  }
  
  private func getPercentEcododedString(_ string: String) -> String {
    string.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
  }
}

@main
struct MyWidget: Widget {
  let kind: String = "MyWidget"
  
  var body: some WidgetConfiguration {
    IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
      MyWidgetEntryView(entry: entry)
    }
    .configurationDisplayName("위젯 예제")
    .description("이미지를 불러오는 위젯 예제입니다")
  }
}
  • getPhoto(completion:) 부분구현
    • API 호출
    • 특정 url의 캐싱이 존재하면 ImageCache.shared.object(forKey:), 그 이미지 사용
    • 캐싱이 안되어있으면, url에 해당하는 데이터를 불러온 후, uiImage로 변환
  private func getPhoto(completion: @escaping (UIImage, String) -> ()) {
    guard
      let url = URL(string: "https://api.flickr.com/services/feeds/photos_public.gne?tags=texas&tagmode=any&format=json&nojsoncallback=1")
    else { return }
    URLSession.shared.dataTask(with: url) { data, response, error in
      guard
        let data = data,
        let photoModel = try? JSONDecoder().decode(PhotoModel.self, from: data),
        let urlString = photoModel.url
      else { return }
      
      if let uiImage = ImageCache.shared.object(forKey: urlString as NSString) {
        completion(uiImage, urlString)
      } else {
        guard
          let url = URL(string: urlString),
          let data = try? Data(contentsOf: url),
          let uiImage = UIImage(data: data)
        else { return }
        ImageCache.shared.setObject(uiImage, forKey: urlString as NSString)
        completion(uiImage, urlString)
      }
    }.resume()
  }

완성)

* 전체 코드: https://github.com/JK0369/ExPhotoWIdget

* 참고

https://stackoverflow.com/questions/63086029/ios-widgetkit-remote-images-fails-to-load

 

Comments