iOS開発に大改革、SwiftUIチュートリアル1 〜Viewの作成〜

こんにちは。 WWDCの2日目に参加しております、iOSエンジニアの齋藤です。

いやー昨日のKeynoteで大盛り上がりでしたね。なかでも最も盛り上がったのは最後に発表されたこれでしょう。

UI作成の新たなフレームワーク、SwiftUIです。

f:id:kohei1218:20190604121533p:plain
引用:Apple

SwiftUIがどのようなものかは自分の昨日の記事やAppleの公式を参照すればわかりやすいかと思います。
2日目の今日11時からSwiftUIについてのセッションがあったのですが超満員でした。この記事ではSwiftUIがもたらす変化と、実際に触ってみた感触をみなさんにお伝えします。
※developerアカウントが必要ない、一般に公開されている内容になります。

SwiftUIが変えるUI開発の未来

InterfaceBuilderとの決別を感じさせるほど、UIの作成は楽になりました。
リアルタイムでViewを確認できるのも大きく、開発効率は飛躍的に上がると感じます。

ただ、複雑なUIの作成にどこまで対応できるのかは今後の課題になりうる気がします。(ReactNativeやFlutterで言われていた、UIにこだわりたいならSwiftで作るよねという状態にならないか)

ダークモードの対応にも柔軟にできて、Appleが今後はUIKitとInterfaceBuilderを完全に無くしたいのかなと思うほど、本気を感じました。

今世紀最大の盛り上がり!!WWDC2019で発表された、すぐにでも使いたい3つの新技術

Xcode - SwiftUI - Apple Developer

SwiftUIチュートリアル1をさわっていく

Appleがチュートリアルを用意してくれているのでそれを触っていきましょう。

Creating and Combining Views

必要環境

・Xcode 11 beta(必須)
・macOS Catalina 10.15 beta(できたら)

こちらからダウンロードできます。 Beta Software Downloads

Xcode 11 betaがあればSwiftUIは使用できますが 目玉機能でもあるGUI上からUIパーツを確認できたり変更をリアルタイムでGUIで見ることのできるCanvasを使用することができないのでできたらmacOS Catalina 10.15 betaも入れてやりましょう。

※ただbeta版なので必ずバックアップを取ってからインストールしましょう。

プロジェクトを新規作成してuse SwiftUIにチェックを入れる。

開いてみるとAppDelegateの他にSceneDelegateContentViewというのができています。 SceneDelegateの中身をみてみるとwillConnectToというメソッドがあり、ここにrootにContentViewをセットしています.

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Use a UIHostingController as window root view controller
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIHostingController(rootView: ContentView())
        self.window = window
        window.makeKeyAndVisible()
    }

ではContentViewを見ていきましょう。

import SwiftUI

struct ContentView : View {
    var body: some View {
        Text("Hello World")
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

コード量が少なすぎて驚きました。

この状態でCanvasの右上にあるResumeをおすとビルドが走り、Viewが表示されます。

ここでTextの文字列を変更したらCnavasの方のTextも即時に変更されました。すごい。

CnavasからUIパーツを、またはエディタからTextをcmdを押しながら選択するとInspectやembedなどを変更でき、それがそれぞれに即反映されます。

試しにListを選択するとそれだけでTableViewのUIを作成できました.

ここからはチュートリアル通り進めていきます。

Textの作成

TextをEmbed in VStackを選択してVerticalのStackViewの入れ子にします。
その後、右上の+ボタンを選択しTextを先ほどのStackViewに追加します。

続いて先ほど追加したTextをこのような構成にしていきます。

f:id:kohei1218:20190605111100p:plain
引用:Apple

HStackはHorizontalのStackView、Spacerはスペースを空けたいviewの間にSpacerを差し込むだけスペースができます。
公式ではSpacerについてこのように記述しています。

A spacer expands to make its containing view use all of the space of its parent view, instead of having its size defined only by its contents.

要は親ビューのサイズまで広がりますよってことみたいです。

まず最後に追加したTextをEmbed in HStackを選択し、HoraizontalのStackViewの入れ子にします。
続いてSpacerを差し込んで、Textを追加します。 最後に親のVStackのalignmentをleadingに変更し、paddingをつけてあげるだけでこのようなviewが完成します。

丸いImageの作成

続いて丸いImageを作成していきます。
New File->SwiftUI で新しいファイルを作成します。
Imageを設置して、Shape、Border、Shadowを設定していきます。

import SwiftUI

struct CircleImage : View {
    var body: some View {
        Image(uiImage: #imageLiteral(resourceName: "turtlerock.jpg"))
            .clipShape(Circle())
            .overlay(
                Circle().stroke(Color.white, lineWidth: 4))
            .shadow(radius: 10)
    }
}

#if DEBUG
struct CircleImage_Previews : PreviewProvider {
    static var previews: some View {
        CircleImage()
    }
}
#endif

これで丸いImageを作成できました。

MapViewの作成

続いてImageのうしろに置くMapViewを作成します。
MapではMkMapViewを使用するのですが、SwiftUI内からUIViewサブクラスを使用するにはUIViewRepresentableプロトコルに準拠したSwiftUIビューにラップする必要があります。

import SwiftUI
import MapKit


struct MapView: UIViewRepresentable {
    
    func makeUIView(context: Context) -> MKMapView {
        MKMapView(frame: .zero)
    }
    
    func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MapView>) {
        
    }
}


#if DEBUG
struct MapView_Previews : PreviewProvider {
    static var previews: some View {
        MapView()
    }
}
#endif

updateUiviewはlayoutSubViewsと同じような考えでいいかと思います。ここにMapViewのregionを設定していきます。

    func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MapView>) {
                let coordinate = CLLocationCoordinate2D(
            latitude: 34.011286, longitude: -116.166868)
        let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    }

これでMapViewを作成できました。

あとはこれを組み合わせていきましょう。

組み合わせる

f:id:kohei1218:20190605115533p:plain
引用:Apple
このように組み合わせていきます。
まず先程のContenViewの親のVStackViewをさらにEmbed in VStackViewで入れ子にします。 続いてそのVStackViewにMapViewとCircleImageを追加します。

struct ContentView : View {
    var body: some View {
        VStack {
            MapView()
                .frame(height: 300)
            
            CircleImage()
            
            VStack(alignment: .leading) {
                
                Text("Hello WWDC")
                    .font(.title)
                    .fontWeight(.bold)
                    .color(.red)
                    HStack {
                        Text("Joshua Tree National Park")
                            .font(.subheadline)
                            Spacer()
                            Text("California")
                                .font(.subheadline)
                        }
                        }.padding()
                    }
    }
}

その後、一番下にSpacerを追加して上に詰め、MapViewを上まで広げるためにedgesIgnoringSafeAreaを追加します。
最後にCiercleImageをMapViewに重ねれば終了です。

struct ContentView : View {
    var body: some View {
        VStack {
            MapView()
                .edgesIgnoringSafeArea(.top)
                .frame(height: 300)
            
            CircleImage()
                .offset(y: -130)
                .padding(.bottom, -130)
            
            VStack(alignment: .leading) {
                
                Text("Hello WWDC")
                    .font(.title)
                    .fontWeight(.bold)
                    .color(.red)
                HStack {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
                }.padding()
            Spacer()
        }
    }
}

お疲れ様でした、いかがだったでしょうか?
ほとんどコードを書かずにここまでのviewをこれだけのコードで作成できるのはまさに革命的だなと感じます。
Listの作成に続きます

ここまで簡単になるのか、SwiftUIチュートリアル2 〜Listの作成〜