관리 메뉴

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

[iOS - SwiftUI] 3. 위젯 Widget 사용 방법 - 위젯 딥링크 구현 방법 (widgetURL, scenePhase, sheet) 본문

카테고리 없음

[iOS - SwiftUI] 3. 위젯 Widget 사용 방법 - 위젯 딥링크 구현 방법 (widgetURL, scenePhase, sheet)

jake-kim 2022. 10. 3. 23:27

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

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

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

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

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

Widget 준비

import WidgetKit
import SwiftUI
import Intents

struct Provider: IntentTimelineProvider {
  func placeholder(in context: Context) -> SimpleEntry {
    SimpleEntry(date: Date(), texts: ["Empty"])
  }
  
  func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    getTexts { texts in
      let entry = SimpleEntry(date: Date(), texts: texts)
      completion(entry)
    }
  }
  
  func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    getTexts { texts in
      let currentDate = Date()
      let entry = SimpleEntry(date: currentDate, texts: texts)
      let nextRefresh = Calendar.current.date(byAdding: .minute, value: 3, to: currentDate)!
      let timeline = Timeline(entries: [entry], policy: .after(nextRefresh))
      completion(timeline)
    }
  }
  
  // 메소드 추가
  private func getTexts(completion: @escaping ([String]) -> ()) {
    // https://github.com/wh-iterabb-it/meowfacts
    guard
      let url = URL(string: "https://meowfacts.herokuapp.com/?count=1")
    else { return }
    URLSession.shared.dataTask(with: url) { data, response, error in
      guard
        let data = data,
        let textModel = try? JSONDecoder().decode(TextModel.self, from: data)
      else { return }
      completion(textModel.datas)
    }.resume()
  }
}

struct SimpleEntry: TimelineEntry {
  let date: Date
  let texts: [String]
}

// 모델 추가
struct TextModel: Codable {
  enum CodingKeys : String, CodingKey {
    case datas = "data"
  }
  let datas: [String]
}

struct MyWidgetEntryView : View {
  var entry: Provider.Entry
  
  private var randomColor: Color {
    Color(
      red: .random(in: 0...1),
      green: .random(in: 0...1),
      blue: .random(in: 0...1)
    )
  }
  
  var body: some View {
    ZStack {
      randomColor.opacity(0.7)
      ForEach(entry.texts, id: \.hashValue) { text in
        LazyVStack { // Widget은 스크롤이 안되므로, List지원 x (대신 VStack 사용)
          Text(text)
            .foregroundColor(Color.white)
            .lineLimit(1)
          Divider()
        }
      }
    }
  }
}

@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("랜덤 텍스트를 불러오는 위젯 예제입니다")
  }
}

struct MyWidget_Previews: PreviewProvider {
  static var previews: some View {
    MyWidgetEntryView(entry: SimpleEntry(date: Date(), texts: ["empty"]))
      .previewContext(WidgetPreviewContext(family: .systemSmall))
  }
}

위젯 딥링크 처리 - 송신하는쪽

  • Widget 코드에서 뷰에다가 .widgetURL(_:)를 선언하여 딥링크 송신
struct MyWidgetEntryView : View {
  ...
  
  var body: some View {
    ZStack {
      randomColor.opacity(0.7)
      ForEach(entry.texts, id: \.hashValue) { text in
        LazyVStack {
          Text(text)
            .foregroundColor(Color.white)
            .lineLimit(1)
            .widgetURL("여기서 딥링킹") // <-
        }
      }
    }
  }
}
  • widgetURL에 주입될 딥링크 포멧 정의
    • scheme: widget
    • host: deeplink
    • query: text
widget://deeplink?text={content}
  • query에 text를 넣는데, text에는 띄어쓰기와 한글이 있으면 URL 변환에 실패하므로 percent encoding이 필요
  private func getPercentEcododedString(_ string: String) -> String {
    string.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
  }
  • 위 함수를 사용
	Text(text)
		.foregroundColor(Color.white)
		.lineLimit(1)
		.widgetURL(URL(string: getPercentEcododedString("widget://deeplink?text=\(text)")))

딥링크 처리 - 수신하는쪽

  • @main 엔트리 포인트인 곳의 WindowGroup 하위에 있는 뷰 바로 밑에 onOpenURL 을 추가하여 처리
    • 보낼때 percent encoding을 했으므로 받는곳에서 removingPercentEncoding을 하여 다시 변환
@main
struct RandomTextApp: App {
  
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onOpenURL { url in
            let text = url.absoluteString.removingPercentEncoding ?? ""
        }
    }
  }
}
  • 이 url을 ContentView에 전달시켜주어야 하는데, 하위 뷰들에게 전달할때 가장 좋은 방법인 @Environment 사용
    • @Environment 사용하는 구체적인 개념은 이전 포스팅 글 참고
  • @Environment 커스텀 키 정의
    • EnvironmentKey를 준수하는 DeepLinkEnv 구조체 정의
struct DeepLinkEnv: EnvironmentKey {
  static let defaultValue = ""
}
  • @Environment 커스텀 Values 정의
    • 프로퍼티는 deepLinkText
extension EnvironmentValues {
  var deepLinkText: String {
    get { self[DeepLinkEnv.self] }
    set { self[DeepLinkEnv.self] = newValue }
  }
}
  • 적용 방법
    • @main 구조체에 하위에 값을 던져줄 @State 변수 선언
@main
struct RandomTextApp: App {
  @State var text: String = "" // <-
  
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onOpenURL { url in
          text = url.absoluteString.removingPercentEncoding ?? ""
        }
    }
  }
}
  • 하위 뷰인 ContentView에 넘겨주기 위해서 .environment()를 추가하여 전달하고 onOpenURL에서 text 입력
@main
struct RandomTextApp: App {
  @State var text: String = ""
  
  var body: some Scene {
    WindowGroup {
      ContentView()
        .environment(\.deepLinkText, text) // <-
        .onOpenURL { url in
          text = url.absoluteString.removingPercentEncoding ?? "" // <-
        }
    }
  }
}
  • ContentView에서는 @Environment를 선언하여, 적용
struct ContentView: View {
  @Environment(\.deepLinkText) var deepLinkText: String
  
  var body: some View {
    if deepLinkText.isEmpty {
      Text("Hello World")
    } else {
      Text(deepLinkText)
    }
  }
}

완성)

딥링크 + .sheet

  • 딥링크가 오면 그에 해당하는 페이지를 띄워주는 형태로 구현 방법?

  • isPresented가 true이면 sheet가 표출되고, isPresented는 onOpenURL안에서 상태의 값을 변경하도록 처리
  • 필요한 상태 변수 선언
@main
struct RandomTextApp: App {
  @State var text: String = ""
  @State var isPresented: Bool = false
  
}
  • body에서 딥링크 처리
    • onOpenURL: 딥링크 수신 처리, 여기서 isPresented = true로 설정
    • .sheet(): isPresented에 상태에 따라 딥링크 데이터를 표출할지 결정 (dismiss시에 딥링크 데이터를 초기화)
@main
struct RandomTextApp: App {
  @State var text: String = ""
  
  var body: some Scene {
    WindowGroup {
      ContentView()
        .environment(\.deepLinkText, text)
        .onOpenURL { url in
          text = url.absoluteString.removingPercentEncoding ?? ""
          isPresented = true
        }
        .sheet(
          isPresented: $isPresented,
          onDismiss: {
            text = ""
          },
          content: {
            if !text.isEmpty {
              Text(text)
                .font(.title)
                .foregroundColor(.primary)
            }
          }
        )
    }
  }
}

cf) 꼭 최상위 뷰인 @main에서 onOpenURL을 놓지 않고, 어느 뷰에서든 onOpenURL을 붙여서 편리하게 처리가 가능

 

* 전체 코드: https://github.com/JK0369/ExRandomText/tree/deepLink

Comments