ios

[ios - SwiftUI] Widget 사용하기 (2/2)

POKY_0908 2021. 2. 4. 15:37

 

 

 

위 이미지는 뉴스 API를 이용해 받아온 데이터 리스트를 선택하면 해당 뷰로 이동할 수 있도록 제작한 위젯입니다.

 

이번 글에서 코드를 살펴보도록 하겠습니다.

 

 

 

struct myStaticWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Color(.systemOrange)
            Text("최신 뉴스 20")
        }
    }
}
struct myStaticWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        ZStack {
            Color(.systemOrange)
            VStack {            
                Text("최신 뉴스 20")
            }
        }
    }
}

 

뷰 백그라운드의 색상을 변경하기 위해선 ZStack에서 Color를 추가해 줘야 합니다. 

 

아래 이미지를 통해 VStack과 ZStack에 배경색을 넣었을 때 차이를 확인할 수 있습니다.

 

 

 

배경색을 변경했으니 이제 리스트를 추가하려고 하는데 위젯에서는 List를 지원하지 않는다고 합니다.

 

 

 

리스트를 추가해보면 위와 같은 모습을 볼 수 있는데 이유를 찾아보니 위젯은 스크롤을 지원하지 않아서 사용할 수 없다는 것 같습니다. 

 

위에서 사용한 VStack을 리스트처럼 사용하도록 합니다.

 

 

struct RowView : View {
    let index: Int
    let name: String
    
    var body: some View {
        Text(name)
        Divider()
    }
}

리스트에 행에 해당하는 뷰를 만듭니다. 텍스트는 뉴스의 타이틀을 보여줄 예정이며

Divider를 사용해 텍스트 아래 구분선을 나타냅니다.

 

 

이제 위젯의 크기에 따라 리스트를 보여주도록 하겠습니다.

struct SimpleEntry: TimelineEntry {
    let date: Date /// 필수 프로퍼티
    let news: News
}

 

TimelineEntry에 News Class을 추가해줬습니다. 사용하는 데이터에 따라 세팅해주면 됩니다. 

 

 

func getNewData(completion: @escaping (News) -> ()) {
    let newsAddress: String = "http://newsapi.org/v2/top-headlines?country=kr&apiKey=75c9a397ffa14b8694f458a03c5342cf"  
    let task = URLSession.shared.dataTask(with: URL(string: newsAddress)!) { (data, response, errpr) in
        if let jsonData = data {     
            if let news = try? JSONDecoder().decode(News.self, from: jsonData) {
                completion(news)
            }
        }
    }
    task.resume()
}

 

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    getNewData { (news) in
        let entry = SimpleEntry(date: Date(), news: news)
        completion(entry)
    }
}

위젯을 추가할 때 데이터를 미리 볼 수 있도록 하였습니다.

 

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    getNewData { (news) in
        let date = Date()
        let entry = SimpleEntry(date: date, news: news)
        let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: date)
        let timeline = Timeline(entries: [entry], policy: .after(nextUpdate!))
            
        completion(timeline)
    }
}

 

 

struct myStaticWidgetEntryView : View {
    @Environment(\.widgetFamily) private var widgetFamily
    var entry: Provider.Entry
    
    var body: some View {
        ZStack {
            Color(.systemOrange)
            VStack {
                switch widgetFamily {
                case .systemSmall:
                    Text("최신 뉴스 \(entry.news.articles?.count ?? 0)").foregroundColor(.white)
                case .systemMedium:
                    if let data = entry.news.articles {
                        ForEach(0..<4) { index in
                            RowView(index: index, name: data[index].title!).padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)).foregroundColor(.white)
                        }
                    }
                case .systemLarge:
                    if let data = entry.news.articles {
                        ForEach(data, id:\.title) { item in
                            Text(item.title!).padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)).foregroundColor(.white)
                            Divider()
                        }
                    }
                }
            }
        }
    }
}

위젯의 크기를 알기 위해서 @Environment(\. widgetFamily) private var widgetFamily를 선언 후 switch 문으로 분기합니다.

 

ForEach를 사용해 News API로 얻은 리스트를 RowView로 추가하면 리스트가 완성됩니다.

 

 

 

 

이제 홈화면에 위젯을 추가하고 로우를 선택 시 해당 선택된 로우의 정보를 뷰에서 확인할 수 있도록 하겠습니다.

 

 

 

struct RowView : View {
    let index: Int
    let name: String
    
    var body: some View {

        let url = URL(string: "widget://key?Params=\(index)")!
        Link(destination: url) {
            Text(name)
            Divider()
        }
    }
    
}

Link를 추가해 각각의 리스트에서 앱의 appDelegate 또는 sceneDeleagate의 openURL 이동할 수 있도록 합니다.

 

위젯 전체를 눌러 딥링킹 하고자 한다면 widgetURL을 사용하면됩니다.

 

VStack {
    Text(name)
    Divider()
}.widgetURL(URL(string: "widget://key?Params=\(index)"))

 

 

이제 sceneDeleagate로 이동해서 아래 소스를 추가해 줍니다.

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    if let url = URLContexts.first?.url {
        if url.absoluteString.starts(with: "widget://key") {
            guard let urlComponents = URLComponents(string: url.absoluteString) else { return }
            guard let requestParams = urlComponents.queryItems?.first(where: { $0.name == "Params" })?.value else { return }
            guard let requestValue = urlComponents.queryItems?.first(where: { $0.name == "Value" })?.value else { return }
                
            let storyboard = UIStoryboard(name: "Main", bundle: nil)
            let vc = storyboard.instantiateViewController(withIdentifier: "ViewController2") as! ViewController2
            vc.data = requestParams
                
            self.window?.rootViewController = vc
            self.window?.makeKeyAndVisible()
        }
    }
}

 

URL에 있던 index를 뷰 컨트롤 프로퍼티에 세팅하고 인덱스에 해당하는 뉴스를 보여주면 됩니다. 

 

저는 인덱스만 표시하도록 했습니다.

 

위젯에서 선택한 키를 확인

 

 

 

@main
struct myStaticWidget: Widget {
    let kind: String = "myStaticWidget"

    /// kind - Widget의 고유 식별자
    /// provider - 새로고침할 타임라인 결정
    
    /// StaticConfiguration 사용자가 프로퍼티를 구성할 수 없다. <- 그냥 보여주기용
    /// IntentConfiguration 사용자가 프로퍼티를 구성할 수 있다. <- 보고싶은것을 선택하는 것 등
    var body: some WidgetConfiguration {
        	
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            myStaticWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget") // 위젯 추가, 편집시 표시 이름
        .description("This is an example widget.") // 위젯에 표시되는 설명
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) // 사이즈 지원
    }
}

위젯이 지원하는 사이즈를 선택하려면 supportedFamilies([.systemSmall, .systemMedium, .systemLarge])를 사용하면 됩니다.