SwiftUI+URLSession+CombineでAPI叩いてみた

# はじめに
趣味でSwiftを書いている@KaitoKudouです。
これまでSwiftUIは学習したことがなく、なんとなくやってみようと思い、SwiftUIを使って外部APIを叩いてみようと思いました。どうせならやったことないCombineフレームワークもちょっとかじってみました。(ほんのちょっとだけ)

# デモアプリ概要
今回は[Connpass API](https://connpass.com/about/api/)を使用してListでイベントを表示する簡易的なデモアプリを作りました。

先に完成版をお見せします。
![da17f00acdb28498565f0c2a0f7f6331.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425368/b60a2a17-c8d4-e84e-cc34-2f333f51db84.gif)
今回作成のプロジェクトはGitHubに上げています。
https://github.com/KaitoKudou/SwiftUI_Combine

# モデルを定義
レスポンスを見ながらモデルを書きます。

```Swift
import Foundation

struct ConnpassGeneral: Codable {
    let events: [Event]
}

struct Event: Codable, Hashable, Identifiable {
    let id: Int
    let title: String
    let eventUrl: String
    
    enum CodingKeys: String, CodingKey {
        case id = "event_id"
        case title = "title"
        case eventUrl = "event_url"
    }
}
```
# 通信部分

```Swift
import Combine
import Foundation

class ConnpassTopViewModel: ObservableObject {
    var disposable = Set<AnyCancellable>()
    @Published var connpassGeneral: ConnpassGeneral?
    @Published var eventData: [Event]?
    
    init() {
        fetchConpassEvents()
    }
    
    func fetchConpassEvents() {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        let url = URL(string: "https://connpass.com/api/v1/event/?keyword=Swift&order=3")!
        let request = URLRequest(url: url)
        
        URLSession.shared.dataTaskPublisher(for: request)
            .map({ (data, response) in
                return data
            })
            .decode(type: ConnpassGeneral.self, decoder: decoder)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                switch completion {
                case .failure(let error):
                    print("error ： " + error.localizedDescription)
                case .finished:
                    print("----------success-----------")
                }
            }, receiveValue: { [weak self] connpassGeneral in
                self?.eventData = connpassGeneral.events
                print(self?.eventData as Any)
            })
            .store(in: &disposable)
    }
}
```

# List部分
UIKitでいうUITableViewにあたる部分です。

```Swift
import SwiftUI

struct ConnpassTopView: View {
    @ObservedObject var connpassTopViewModel = ConnpassTopViewModel()
    
    var body: some View {
        NavigationView {
            List(connpassTopViewModel.eventData ?? [Event.init(id: 0, title: "hoge", eventUrl: "hoge")]) { event in
                NavigationLink(
                    destination: ConnpassEventDetailView(eventData: event)) {
                    ConnpassEventRowView(eventData: event)
                }
            }
            .navigationTitle("Connpassイベント")
        }
        .onAppear(perform: {
            self.connpassTopViewModel.fetchConpassEvents()
        })
    }
}

struct ConnpassTopView_Previews: PreviewProvider {
    static var previews: some View {
        ConnpassTopView(connpassTopViewModel: ConnpassTopViewModel())
    }
}
```
# セルを押したときの遷移先
今回はセルを押したらSFSafariViewControllerを使ってSafariへ遷移するようにしました。しかし、SFSafariViewControllerはUIKitで使えるクラスになります。SwiftUIのViewでUIKitを使うには[ UIViewControllerRepresentable](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable)プロトコルに準拠すると使えます。
UIViewControllerRepresentableプロトコルでは、makeUIViewController(context:)とupdateUIViewController(_:context:)が必須なメソッドとなります。

```Swift
import SafariServices
import SwiftUI

struct ConnpassEventDetailView: UIViewControllerRepresentable {
    
    var eventData: Event
    typealias UIViewControllerType = SFSafariViewController
    
    func makeUIViewController(context: Context) -> SFSafariViewController {
        let url = URL(string: eventData.eventUrl)
        return SFSafariViewController(url: url!)
    }
    
    func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {
    }
}

struct ConnpassEventDetailView_Previews: PreviewProvider {
    static var eventData = ConnpassTopViewModel().eventData
    static var previews: some View {
        ConnpassEventDetailView(eventData: eventData?[0] ?? Event.init(id: 0, title: "hoge", eventUrl: "hoge"))
    }
}
```

# 最後に
全体的に少ないコード量で済みました。SwiftUIはUIKitに比べると記述量が減り、書きやすいと思いました。まだ私はUIKitの方が実装にかかる時間が短いですが、SwiftUIに慣れたら開発が爆速で進むと思いました。Combineに関してはまだまだ知識が足りないので精進します...
既存のライブラリもSwiftUIに対応したものは少なく、まだしばらくはUIKitを使った開発が主流なのかなと思いました。