FJCT_ニフクラ mobile backend(mBaaS)お役立ちブログ

スマホアプリ開発にニフクラ mobile backend(mBaaS)。アプリ開発に役立つ情報をおとどけ!

Swift SDKを使ってTodoアプリを作る(その1:ベースアプリの解説)

f:id:mbaasdevrel:20210407110753p:plain

NCMBではSwift向けにSDKをリリースしています。最近ではiOSアプリを作る際にSwiftを利用することが増えていますので、その場合にはネイティブ言語で実装されているSwift SDKの利用をお勧めします。

そんなSwift SDKの使い方を知ってもらうためにも、ちょっとしたアプリ(Todoアプリ)を作ってみました。徐々に機能を追加していきますので、Swift SDKの利用法をぜひ覚えてください。

Swift SDKの導入法

Swift SDKはCocoaPods向けに提供しています。Xcodeで新しいiOSプロジェクトを作ったら、Podfileを用意します。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'todoapp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for todoapp
  pod 'NCMB', :git => 'https://github.com/NIFCLOUD-mbaas/ncmb_swift.git'
end

そして pod install でSwift SDKをインストールします。

初期化について

現在、新規でiOSアプリを作成すると、InterfaceがSwiftUI、Life CycleがSwiftUI Appとなっています。この状態で作ると AppDelegate.swift はなく、 (アプリ名)App.swift というファイルが作られます。

この場合、まずSwift SDKを読み込みます。

import SwiftUI
import NCMB   // 追加

そして var body の上に @Environment を追加します。

@Environment(\.scenePhase) private var scenePhase // 追加
var body: some Scene {

最後に WindowGrouponChange を追加します。このonChangeの中で初期化処理を行います。

WindowGroup {
    ContentView()
}
.onChange(of: scenePhase) { scene in
    switch scene {
    case .active:
        NCMB.initialize(applicationKey: "YOUR_APPLICATION_KEY", clientKey: "YOUR_CLIENT_KEY")
    }
}

これで利用可能になります。

UIについて

f:id:mbaasdevrel:20210407110753p:plain

UIは全部で2つです。1つはタスクが一覧される画面です。これはNCMBからデータを取得して、一覧表示します。データ形式が単純ではないので ObservableObject を利用しています。

import SwiftUI
import NCMB
import Combine

class Todos: ObservableObject {
    @Published var todos: [NCMBObject] = []
}

メイン画面の表示部分です。onAppear でNCMBのTodoクラスへ問い合わせて、その結果を画面に反映しています。

struct ContentView: View, InputViewDelegate {
    @ObservedObject var Todo = Todos()
    var body: some View {
        NavigationView {
            ZStack(alignment: .bottomTrailing) {
                List {
                    ForEach(self.Todo.todos, id: \.objectId) { todo in
                        Text((todo["body"] ?? "") as String)
                    }
                    .onDelete(perform: delete)
                }
                
                NavigationLink(destination: InputView(delegate: self, text: "")) {
                    Text("Add")
                        .foregroundColor(Color.white)
                        .font(Font.system(size: 20))
                }
                .frame(width: 60, height: 60)
                .background(Color.blue)
                .cornerRadius(30)
                .padding()
                
            }
            .onAppear {
                let query : NCMBQuery<NCMBObject> = NCMBQuery.getQuery(className: "Todo")
                query.findInBackground(callback: { result in
                    switch result {
                        case let .success(array):
                            DispatchQueue.main.async {
                                self.Todo.todos = array
                            }
                        case let .failure(error):
                            print("取得に失敗しました: \(error)")
                    }
                })
            }
            .navigationTitle("TODO")
            .navigationBarItems(trailing: EditButton())
        }
    }
}

追加されるメソッドとして、タスクの追加と削除があります。追加は次のようになります。NCMBObjectはsaveメソッドで行います。

func addTodo(text: String) {
    let obj = NCMBObject(className: "Todo")
    obj["body"] = text
    let result = obj.save()
    switch result {
        case .success(_):
            self.Todo.todos.append(obj)
        case let .failure(error):
            print("取得に失敗しました: \(error)")
    }
}

同じく削除はNCMBObjectのdeleteメソッドを使います。

func delete(at offsets: IndexSet) {
    let todo = self.Todo.todos[Array(offsets)[0]] as NCMBObject
    let result = todo.delete()
    switch result {
        case .success(_):
            self.Todo.todos.remove(atOffsets: offsets)
        case let .failure(error):
            print(error)
    }
}

タスクの追加画面

f:id:mbaasdevrel:20210407110820p:plain

続いてタスクの追加ですが、次のようにInputViewで行います。呼ぶのは上記のaddTodoになります。

protocol InputViewDelegate {
    func addTodo(text: String)
}

struct InputView: View {
    @Environment(\.presentationMode) var presentation
    let delegate: InputViewDelegate
    @State var text: String
    var body: some View {
        VStack(spacing: 16) {
            TextField("タスクを追加してください", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            Button("追加") {
                delegate.addTodo(text: text)
                presentation.wrappedValue.dismiss()
            }
        }
        .padding()
    }
}

全体のコードは次のようになります。

//
//  ContentView.swift
//  todoapp
//
//  Created by Atsushi on 2021/04/05.
//

import SwiftUI
import NCMB
import Combine

class Todos: ObservableObject {
    @Published var todos: [NCMBObject] = []
}

struct ContentView: View, InputViewDelegate, EditViewDelegate {
    func editTodo(text: String) {
    
        print(text)
    }
    
    @ObservedObject var Todo = Todos()
        
    var body: some View {
        NavigationView {
            ZStack(alignment: .bottomTrailing) {
                List {
                    ForEach(self.Todo.todos, id: \.objectId) { todo in
                        NavigationLink(destination: EditView(delegate: self, text: (todo["body"] ?? "") as String)) {
                            Text((todo["body"] ?? "") as String)
                        }
                    }
                    .onDelete(perform: delete)
                }
                
                NavigationLink(destination: InputView(delegate: self, text: "")) {
                    Text("Add")
                        .foregroundColor(Color.white)
                        .font(Font.system(size: 20))
                }
                .frame(width: 60, height: 60)
                .background(Color.blue)
                .cornerRadius(30)
                .padding()
                
            }
            .onAppear {
                let query : NCMBQuery<NCMBObject> = NCMBQuery.getQuery(className: "Todo")
                let result = query.find()
                switch result {
                    case let .success(array):
                        self.Todo.todos = array
                    case let .failure(error):
                        print("取得に失敗しました: \(error)")
                }
            }
            .navigationTitle("TODO")
            .navigationBarItems(trailing: EditButton())
        }
    }
    
    func delete(at offsets: IndexSet) {
        let todo = self.Todo.todos[Array(offsets)[0]] as NCMBObject
        let result = todo.delete()
        switch result {
            case .success(_):
                self.Todo.todos.remove(atOffsets: offsets)
            case let .failure(error):
                print(error)
        }
    }
    
    func addTodo(text: String) {
        let obj = NCMBObject(className: "Todo")
        obj["body"] = text
        var acl = NCMBACL.empty
        let user = NCMBUser.currentUser
        if user != nil {
            acl.put(key: user?.objectId! ?? "", readable: true, writable: true)
            acl.put(key: "role:Admin", readable: true, writable: false)
            obj.acl = acl
        }
        
        let result = obj.save()
        switch result {
            case .success(_):
                self.Todo.todos.append(obj)
            case let .failure(error):
                print("取得に失敗しました: \(error)")
        }
    }
}

protocol EditViewDelegate {
    func editTodo(text: String)
}

struct EditView: View {
    @Environment(\.presentationMode) var presentation
    let delegate: EditViewDelegate
    @State var text: String
    var body: some View {
        VStack(spacing: 16) {
            TextField("タスクを編集してください", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            Button("追加") {
                delegate.editTodo(text: text)
                presentation.wrappedValue.dismiss()
            }
        }
        .padding()
    }
}

protocol InputViewDelegate {
    func addTodo(text: String)
}

struct InputView: View {
    @Environment(\.presentationMode) var presentation
    let delegate: InputViewDelegate
    @State var text: String
    var body: some View {
        VStack(spacing: 16) {
            TextField("タスクを追加してください", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            Button("追加") {
                delegate.addTodo(text: text)
                presentation.wrappedValue.dismiss()
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

まとめ

TodoアプリはデータストアのCRUD操作を基本としていますので、Swift SDKの使い方を覚えるのに最適かと思います。次はこのアプリに匿名認証機能を追加する方法を紹介します。

中津川 篤司

中津川 篤司

NCMBエヴァンジェリスト。プログラマ、エンジニアとしていくつかの企業で働き、28歳のときに独立。 2004年、まだ情報が少なかったオープンソースソフトの技術ブログ「MOONGIFT」を開設し、毎日情報を発信している。2013年に法人化、ビジネスとエンジニアを結ぶDXエージェンシー「DevRel」活動をスタート。