【SwiftUI】物理的に平成を振り返るアプリを開発した

#はじめに
今回はサイバーエージェント主催の平成最後のハッカソンで開発した「平成振り返りデバイス」のiOSアプリ版を最近発表されたSwiftUIで開発したぜ的な記事になります。![22909_ext_03_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256520/b7faa805-575f-a2d7-c56f-6b7e3fc24d75.png)

#平成振り返りデバイスとは
以下の写真にあるデバイスを体に身に付けて物理的に振り返るとWebサイトに平成のニュースやtwitterのバズったツイートが届くサービスになっています。デモ動画は[こちら](https://vimeo.com/333354788)にあります。![IMG_20190430_132455.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256520/6b3afec0-c8fa-606e-2cba-5d907c030254.jpeg)

#平成振り返りアプリ
平成振り返りデバイスは手元にデバイスが無いと遊べないため、同じようなアプリを作ってみんなに遊んでもらいたい！と思い開発しました。以下のようにiPhoneのジャイロセンサで振り向きを検知したら平成のニュースやツイートが届きます。
![ダウンロード.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256520/8e4bbe21-acb2-4087-55ee-81378159f684.gif)

#開発環境
* iOS13
* Xcode11
* SwiftUI

#View
簡単にソースコードの紹介をします。

##CardView
ニュースを表示させるカードのソースコードです。これをList表示します。

```CardView.swift
import SwiftUI

struct CardView : View {
    var newsModel: NewsModel!
    var body: some View {
        VStack {
            HStack {
                Spacer()
                Image(uiImage: self.getImageByUrl(url: newsModel.img))
                    .resizable()
                    .frame(width: 300, height: 200)
                    .aspectRatio(contentMode: .fit)
                Spacer()
            }
                Text(newsModel!.title)
                    .font(.title)
            Text("date: \(newsModel.date)")
                    .font(.subheadline)
                    .color(.gray)
            Text("ジャンル: \(newsModel.genre)")
                    .font(.subheadline)
                    .color(.gray)
                PresentationButton(
                    Text("READ MORE")
                        .color(.blue)
                    , destination: ReadMoreView(url: newsModel.url))
                }
                .padding(.top)

    }
    
    func getImageByUrl(url: String) -> UIImage{
        let url = URL(string: url)
        do {
            let data = try Data(contentsOf: url!)
            return UIImage(data: data)!
        } catch let err {
            print("Error : \(err.localizedDescription)")
        }
        return UIImage()
    }
    
}
```

##ReadMoreView
ニュースのリンクを表示させるWebViewです。SwiftUIのチュートリアルでMapViewを作っていた感じと同じです。

```ReadMoreView.swift
import SwiftUI
import WebKit

struct ReadMoreView : UIViewRepresentable {
    var url: String!
    
    func makeUIView(context: Context) -> WKWebView {
        WKWebView(frame: .zero)
    }
    
    func updateUIView(_ view: WKWebView, context: Context) {
        let urlNS = NSURL(string: url)
        let req = NSURLRequest(url:urlNS! as URL)
        view.load(req as URLRequest)
    }
    
}
```

#ViewModel
ViewModelでは振り向きの検知や、ニュースを取得するAPIを叩いたりしてListに表示するデータを保持します。ジャイロセンサで角速度[rad/s]を取得しそれを積分して90度を超えた時にニュースを取得するようにしています。

```LookBackViewModel.swift
import SwiftUI
import Combine
import CoreMotion
import Alamofire

final class LookBackViewModel: BindableObject  {
    let didChange = PassthroughSubject<LookBackViewModel, Never>()
    let motionManager = CMMotionManager()
    let url = [
        "twitter": "https://script.google.com/macros/s/AKfycbyhR8HyQKcf2b9wRUmsCm-6D_EK1zFlJzIpPIhrBuRd49FVFtpT/exec",
        "heisei": "https://script.google.com/macros/s/AKfycbxc-TKyZ8Lp-9Ed05et_wIGw55RLGBGwhNSY2lb2z9iQdy1wLs/exec"]
    var gyro_x: Double! = 0
    var gyro_y: Double! = 0
    var gyro_z: Double! = 0
    var degree:Double! = 0
    
    var imageName = "heisei" {
        didSet {
            didChange.send(self)
        }
    }
    
    var newsList: [NewsModel] = [] {
        didSet {
            didChange.send(self)
        }
    }
    
    init() {
        self.motionCapture()
    }
    
    func request(url: String) {
        Alamofire.request(url,
            method: .get,
            parameters: nil,
            encoding: JSONEncoding.default,
            headers: nil)
            .response { response in
                guard let data = response.data else { return }
                let newsModel: NewsModel = try! JSONDecoder().decode(NewsModel.self, from: data)
                self.newsList.insert(newsModel, at: 0)
        }
    }
    
    func motionCapture() {
        motionManager.deviceMotionUpdateInterval = 0.1
        motionManager.startDeviceMotionUpdates( to: OperationQueue.current!, withHandler:{
            deviceManager, error in
            let gyro: CMRotationRate = deviceManager!.rotationRate
            self.gyro_x = gyro.x
            self.gyro_y = gyro.y
            self.gyro_z = gyro.z
            self.loop(angular: gyro.y)
        })
    }
    
    func loop(angular: Double){
        self.degree += angular * 0.1 * 57.2
        if self.degree >= 90{
            self.imageName = "left"
            self.request(url: self.url["twitter"]!)
            self.degree = 0
        } else if -self.degree >= 90{
            self.imageName = "right"
            self.request(url: self.url["heisei"]!)
            self.degree = 0
        }
    }
    
    
    
}
```

#Model
SwiftのCodableを使ってAPIで取得したニュースのJSONを以下で定義した型に変換します。

```NewsModel.swift
import SwiftUI
import Foundation

struct NewsModel: Hashable, Codable {
    let title: String
    let url: String
    let date: String
    let img: String
    let genre: String
}
```

#おわりに
まだチュートリアルで勉強した程度のUI実装なので汚いですがSwiftUIを使って1つのアプリが開発できた事はすごく自信になりました。これからはよりUIのデザインを洗練しアプリの動作もサクサク動くようにしていきたいです。今回の実装は[GitHub](https://github.com/NakaokaRei/SwiftUI-lookbackheisei)にアップしています。アドバイスなどありましたらコメントお願いします！！！