20201009のiOSに関する記事は11件です。

【ReactNative】SwiftでNativeModuleを書く

ReactNative-0.63.3 XCode-12

はじめに

iOSのネイティブAPIにアクセスするために、ReactNatveにはNative ModuleというAPIが用意されています。
今回は、Swiftでネイティブモジュールを実装する方法をまとめます。

ReactNativeプロジェクトの作成

まだプロジェクトがない場合は、作成します。

npx react-native init myApp

XCodeでSwiftファイルを作成する

ios/myApp.xcodeprojにあるプロジェクトをXCodeで開きます。

NativeModulesフォルダを作成します。(任意)

NativeModulesフォルダ内に.swiftファイルを作成します。

スクリーンショット 2020-10-09 16.15.25.png

Objective-C Bridging Headerを作成する

.swiftファイルを作成すると、XCodeからObjective-C Bridging Headerを作成するかどうか聞かれるの、作成する。

このファイルは、名前の通りSwiftファイルとObjective-Cファイルをブリッジするものです。
ファイル名は変えてはいけません。

以下のように、Objective-C Bridging Headerに追記します。

myApp-Bridging-Header.h
// myApp-Bridging-Header.h

#import"React/RCTBridgeModule.h"

メソッドを実装する

手始めに、最も簡単なネイティブモジュールを実装してみましょう。
カウンターの値を変化させます。

Counter.swift
import Foundation@objc(Counter)
class Counter: NSObject {

  private var count = 0

  @objc
  func increment() {
    count += 1
    print("count is \(count)")
  }
}

メソッドをReactNativeから扱えるようにする

実装したメソッドをReactNativeから扱えるようにObjective-Cのファイルを作成します。

先ほど作成したSwiftファイルと同じ名前で、同ディレクトリに作成します。

スクリーンショット 2020-10-09 16.51.20.png

作成したファイルには以下を追記します。

Counter.m
@interface RCT_EXTERN_MODULE(Counter, NSObject)
  RCT_EXTERN_METHOD(increment)
@end

RCT_EXTERN_METHODについて

RCT_EXTERN_METHODに記述したメソッドは、ReactNativeで使用することができます。

メソッドに引数がない場合は、以下のように記述します。

RCT_EXTERN_METHOD(methodName)

引数がある場合は、以下のように記述します。

RCT_EXTERN_METHOD(
  methodName: (paramType1)internalParamName1
)

例えば、incrementメソッドに引数が必要な場合swiftファイルは以下のようになります。

@objc
func increment(_ num: Int) {
  ...
}

対応するRCT_EXTERN_METHODは以下のようになります。

RCT_EXTERN_METHOD(
  increment: (Int)num
)

ReactNativeから呼び出す

NativeModulesをインポートし、クラス名.メソッドで呼び出すことができます。

import { NativeModules } from 'react-native'

NativeModules.Counter.increment(1));

Event Emitterを扱う

iOSネイティブ側で起こるイベントをSunscribeしたい時には、RCTEventEmitterを使用します。

Objective-Cのファイルに、以下を記述します。

#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"

@interface RCT_EXTERN_MODULE(Counter, RCTEventEmitter)

さらに、ブリッジファイルにも追記します。

CounterApp-Bridging-Header.h
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"

Swiftファイルには以下を実装します。

  • sendEvent: イベント名と、その内容を記述
  • supportedEvents: ReactNative側でリスナーを貼る時のイベント名を記述
@objc(Counter)
class Counter: RCTEventEmitter {
  @objc
  func increment() {
    count += 1
    print("count is \(count)")
    sendEvent(withName: "onIncrement", body: ["count": count])
  }

  override func supportedEvents() -> [String]! {
    return ["onIncrement"]
  }
}

ReactNative側で、イベントリスナーで待ち受けます。

import {
  NativeModules,
  NativeEventEmitter
} from 'react-native'

const CounterEvents = new NativeEventEmitter(NativeModules.Counter);

CounterEvents.addListener(
  "onIncrement",
  (res) => console.log(res)
);

NativeModules.Counter.increment();

[警告] Module requires main queue setupへの対処

以下のような警告が出ることがあります。

スクリーンショット 2020-10-09 22.41.02.png

これは、モジュールの処理をメインスレッドで行うか、バックグラウンドで行うか設定しなさいという警告です。
以下のように記述し、警告を消すことができます。

@objc
static func requiresMainQueueSetup() -> Bool {
  return true
}
  • trueを返す: メインスレッドで処理
  • falseを返す: バックグラウンドで処理

まとめ

ネイティブモジュールをSwiftで書く方法をまとめました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GroupedのUITableViewで一番上の余白を消したい(swift)

Xcode-12.0 iOS-14.0 Swift-5.3

はじめに

下記の画像のように stylegroupedUITableView で条件によって一番上の余白の表示・非表示を切り替えたいときに試行錯誤したのでやり方をメモ。

余白表示 余白非表示
success_1 success_2

今回は画像のようにヘッダー表示時は余白をなくして、ヘッダー非表示の場合は余白ありにしたかった。。。

だめだったパターン

とりあえずだめだったパターン。。。下記のように isHeaderShowntrue のときにセクション0にヘッダーを設定して高さを設定してみた。

extension TableViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        guard section == 0 else {
            return UITableView.automaticDimension
        }
        return isHeaderShown ? 50 : UITableView.automaticDimension
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        guard section == 0 else {
            return nil
        }
        return isHeaderShown ? HeaderView() : nil
    }
}

結果

failure

初回表示だけいけてるけど2回目以降がおかしい。。。:frowning2:

いけてそうなパターン1

上の方法 + 下記のようにテーブル更新時に tableHeaderView をいじってみました。

tableView.tableHeaderView = isHeaderShown ? UIView() : nil
tableView.reloadData()

結果

success

いけてそう:tada:

いけてそうなパターン2

上の方法でもいけてそうですがそもそも一番上にしかヘッダー設定しないなら sectionHeader に View を設定する必要はないと思い最終的には下記のようにしました。

extension TableViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        guard section == 0 else {
            return UITableView.automaticDimension
        }
        return isHeaderShown ? CGFloat.leastNormalMagnitude : UITableView.automaticDimension
    }
}

// 更新時の処理
tableView.tableHeaderView = isHeaderShown ? HeaderView(frame: .init(origin: .zero, size: .init(width: 0, height: 50))) : nil
tableView.reloadData()

結果はパターン1と同じだったのでたぶんいけてそう:clap:

おわりに

こんなレイアウトにしたくなるのはレアだと思いますがどなたかの参考になれば幸いです。他にいい方法ご存知でしたらぜひ教えて下さい:pray:

今後は UITableView より UICollectionView 使っていった方がいいよって言うのも聞くしもうあんまりテーブルをごちゃごちゃするのはよくないのかもしれない。。。:thinking:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcode12でAppStoreConnectへアプリをアップロードするとITMS-90562: Invalid Bundleでビルドが無効になる問題

手元で解決した方法

PodfileからQuickを削除してXCTest使うように変更したら直りました。

問題

今携わってるプロジェクトではスキームを分けて、AppStoreConnectでalpha, staging, productionの3つの環境のアプリを配布しているのですが、Xcode12にアップデートしてアプリを配布しようとしたらalpha, staging環境のアプリでなぜか表題のエラーが起こるようになりAppleからメールが届くようになりました。

ITMS-90562: Invalid Bundle - The app submission can not be successfully recompiled from bitcode due to missing symbols during linking. You can try to reproduce and diagnose such issues locally by following the instructions from: https://developer.apple.com/library/archive/technotes/tn2432/_index.html

スクリーンショット 2020-10-09 19.59.14.png

それとテストもよく分からないエラーを出して失敗するように。

▸ Processing Info.plist
▸ Running script '[CP] Check Pods Manifest.lock'
▸ Processing Info.plist
▸ Running script '[CP] Check Pods Manifest.lock'

❌  error: Illegal instruction: 4 (in target 'XXXXXTests' from project 'XXXXX')


▸ Linking XXXXXUITests
▸ Generating 'XXXXXUITests.xctest.dSYM'
▸ Running script '[CP] Embed Pods Frameworks'
Testing failed:
    Illegal instruction: 4
    Testing cancelled because the build failed.

** TEST FAILED **


The following build commands failed:
    CompileSwift normal x86_64
    CompileSwiftSources normal x86_64 com.apple.xcode.tools.swift.compiler
(2 failures)
[19:14:01]: Exit status: 65

[!] Error building the application. See the log above.

色々試していたのですが、結局解決方法が分からずとりあえずテストで失敗していたのでQuickを使用している部分のテストを削除してみたらテストが通るようになり、そのままXCTestに書き直して再度AppStoreConnectへアップロードしたら表題の問題が起こらないようになりました。

その他

Xcode12でググったりTwitterで検索しても、ワーニングの件や、シミュレータ向けのビルドからarm64アーキテクチャ外す必要があるなどに言及している記事やつぶやきしかひっかからず、困っていました。

一応フォーラムに同じような問題が上がっているのは確認できたのですが、解決はしてなさそう。

あまり起こっていない問題なのかも。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcode12でAppStoreConnectへアプリをアップロードするとITMS-90562でビルドが無効になる問題

手元で解決した方法

PodfileからQuickを削除してXCTest使うように変更したら直りました。

問題

今携わってるプロジェクトではスキームを分けて、AppStoreConnectでalpha, staging, productionの3つの環境のアプリを配布しているのですが、Xcode12にアップデートしてアプリを配布しようとしたらalpha, staging環境のアプリでなぜか表題のエラーが起こるようになりAppleからメールが届くようになりました。

ITMS-90562: Invalid Bundle - The app submission can not be successfully recompiled from bitcode due to missing symbols during linking. You can try to reproduce and diagnose such issues locally by following the instructions from: https://developer.apple.com/library/archive/technotes/tn2432/_index.html

それとテストもよく分からないエラーを出して失敗するように。。。

▸ Processing Info.plist
▸ Running script '[CP] Check Pods Manifest.lock'
▸ Processing Info.plist
▸ Running script '[CP] Check Pods Manifest.lock'

❌  error: Illegal instruction: 4 (in target 'XXXXXTests' from project 'XXXXX')


▸ Linking XXXXXUITests
▸ Generating 'XXXXXUITests.xctest.dSYM'
▸ Running script '[CP] Embed Pods Frameworks'
Testing failed:
    Illegal instruction: 4
    Testing cancelled because the build failed.

** TEST FAILED **


The following build commands failed:
    CompileSwift normal x86_64
    CompileSwiftSources normal x86_64 com.apple.xcode.tools.swift.compiler
(2 failures)
[19:14:01]: Exit status: 65

[!] Error building the application. See the log above.

色々試していたのですが、結局解決方法が分からずとりあえずテストで失敗していたのでQuickを使用している部分のテストを削除してみたらテストが通るようになり、そのままXCTestに書き直して再度AppStoreConnectへアップロードしたら表題の問題が起こらないようになりました。

その他

Xcode12でググったりTwitterで検索しても、ワーニングの件や、シミュレータ向けのビルドからarm64アーキテクチャ外す必要があるなどに言及している記事やつぶやきしかひっかからず、困っていました。

一応フォーラムに同じような問題が上がっているのは確認できたのですが、解決はしてなさそう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcode12で実機デバッグのRunningが遅い

事象

Xcode12にアップデートしてから、実機デバッグ時のRunningに2,3分かかる

解決策

〜/ Library / Developer / Xcode / iOS DeviceSupport
上記パス内のコンテンツを全て削除すると直りました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iosでinputタグがボコってみえるのを直す

課題

まず1枚目と2枚目を比べてみる。
IMG_3121.jpg
IMG_3122.jpg

1枚目はデフォルトのinputタグで上の方にshadowみたいな彫りの深さが出ているバージョン
2枚目はそのiosのデフォルトの仕様に対してcssで彫りの深さを消したバージョン

解決法

inputタグのcssに appearance: none; -webkit-appearance:none; を追加する

css
input {
  appearance: none;
  -webkit-appearance:none;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Array(Element)型を理解しよう!

Array(Element)型について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承ください

Array(Element)型とは?

Array(Element)型を一言でいうと、配列を表す型のことです。

例えば
Array(Element)型は[1,2,3]のように配列リテラルを用いて表現することができます

qiita.rbvar
1.let a = [a,b,c]
2.let b = [1,2,3]

Array(Element)型を用いることで、便利なのが
Array(Element)型の値に対して、要素の更新、追加、結合、削除を行えることです。

それでは一つずつ深堀りしていきましょう!

要素の更新

qiita.rbvar
               //0 1 2 
1.var numbers = [1,2,3]
2.numbers[1] = [4] // 1番目である2の更新を行っている
numbers// [1,4,3]

要素の追加

末尾に要素を追加するにはappend(_:)メソッドを使用します
次の例は、[String]型の"d"を追加していいます

qiita.rbvar
1.let strings  = ["a","b","c"]
2.strings append(d)//["a","b","c","d"]

また、任意の場所に追加することができます。
任意の場所に要素を追加するには、insert(_: at:1)、メソッドを使用します。

次の例では、nsert(_: at:1)***、メソッドをし使用し、2番目に"b"を追加しています。

qiita.rbvar
1.let strings  = ["a","c","d"]
2.strings insert("b",at1)//["a","b","c","d"]

要素の結合

要素の結合は、+演算子でArray(Element)型の値を結合することができます。

qiita.rbvar
1.let strings1 = ["あ","い","う"]//[String型]
2.let c =["え","お"]//[String型]

let result = strings1+strings1//["あ","い","う","え","お"]

要素の削除

要素の削除には、任意の場所を削除するremove(at:)、最後の要素を削除するremoveLast()、全ての要素を削除するremoveAll()メソッドの3つが用意されています。

qiita.rbvar
var strings = ["あ","い","う","え","お"]

strings.remove(at:2)
strings//["あ","い","え","お"]//"う"が削除

strings.removeLast()
strings//["あ","い","え"]//末尾の"お"が削除

strings.removeAll()
strings//()

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

簡単にiOSアプリのアイコンを設定する方法

アイコンの設定

アプリが完成した!と思ったらアイコンの設定をし忘れてた!とやる気を削がれたことはありませんか?この記事では、アプリのアイコンを一瞬で設定する方法をまとめたいと思います。
有名な内容なので、そんなの知ってるよ!と思われた方はそっとブラウザバックをしてください。

アイコンを作ろう!

この記事ではアイコンの作り方自体は書きませんが、僕が普段使っているおすすめのツールを紹介したいと思います。
有料のものばかりになってしまいますが、許して、、、

Sketch
モックアップを作る際やアイコンを作るとき、ロゴを作る時など基本的にSketchを使って作業しています。

Adobe CC
かの有名なAdobe Creative Cloudです。Sketchでは厳しいことなどはイラストレーターやフォトショップを使って作業しています。

それでは本題

ここでようやく本題のアプリのアイコンを一瞬で設定する方法を説明したいと思います。
まず、作成したアプリのアイコンを1024*1024のpngやjpgなどに書き出してください。

こちらのサイトにアクセスして作成した写真を以下の部分にドラックアンドドロップしてください。
スクリーンショット 2020-10-09 13.42.39.png

該当するアプリが対応するプラットフォーム(iPhone、iPadなど)にチェックが入っていることを確認してGenerateを押してダウンロードしてダウンロードされたZipを解凍してください。

解凍できたら、中身がこのようになっていることを確認してください。

スクリーンショット 2020-10-09 13.46.41.png

Xcodeプロジェクトに実際に設定していく

まず、Xcodeプロジェクトを開いて左側にあるファイル一覧からAssets.xcassetsを右クリックしてShow in Finderを選択して下さい。
この時点で一度Xcodeを終了しておくと無難です。

スクリーンショット 2020-10-09 13.48.17.png

このような画面は表示されましたか?

スクリーンショット 2020-10-09 13.49.35.png

このような画面が表示されたら、AppIcon.appiconsetをフォルダーごと削除しましょう!(⌘+Delete)
削除ができたら、先ほどダウンロードしたフォルダーの中にあるAssets.xcassetsの中のAppIcon.appiconsetを今削除したフォルダーがあったAssets.xcassetsにドラック&ドロップしましょう!

スクリーンショット 2020-10-09 13.54.42.png

ここまで完了したら、Xcodeのプロジェクトを再度開いてアイコンが設定されているか確認しましょう!

以上で完了です!お疲れ様でした!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Optional(Wrapped)型を理解しよう!

Optional型について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承ください

Optional(Wrapped)型とは?

Optional(Wrapped)型を一言でいうと、値があるか空かのいずれを表す型です。
基本的に、swiftの変数や定数は基本的にnilを許容しないのですが、そのnilを用いる場合には
Optional(Wrapped)型を使用します!

例えば

qiita.rbvar
1.var n: Int
2.
3.print(n)//エラー

上記のように、存在していない値を出力しようとするとエラーが起こります。
そこで登場するのが、Optional(Wrapped)型です

qiita.rbvar
1.var n: Optional<Int>
2.
3.print(n)//nil

上記のように、Optional(Wrapped)型を使うとエラーにならず、nilが出力がされます。

このように、「『Optional型』でデータをラップしておくと値が存在しない場合は『nil』を返すようになる」という挙動になるためエラーを回避することができます。これが『Optional型』の基本的な使い方になります.

Optional(Wrapped)型のアンラップとは?

Optional(Wrapped)型は値を持っていない可能性があるため、Wrapped型の変数や定数と同じように扱うことができない。
例えば、Int?型どうしの四則演算はエラーになります。

qiita.rbvar
1.let a: Int? = 1
2.let b: Int? = 1
3.a+b//エラー

このエラーを回避するために、アンラップを行います。
アンラップの方法は以下の3つです。

○ オプショナルバインディング
○ ??演算子
○ 強制アンラップ

一つずつ深堀りしていきましょう!

オプショナルバインディングとは?

qiita.rbvar
if let 定数名 = Optional(Wrapped){

//値が存在する場合に実行される文

}

上記の文のように、if-let文を用いてWrapped型の値をもつ場合は{}の文が実行されます。
次の例では、定数Aに値が存在するため、String型の定数aに値が代入され、実行文が実行されます!

qiita.rbvar
1.let A: Optional("1") //Int型
2.if let A {
print(type(of:1))

//実行結果:Int

}

 ??演算子とは?

次の例では、??演算子の左辺にString型の値"a"を持った、String型?の定数optionalStringを右辺にString型の値"b"を指定し、結果として左辺の値"a"を取得しています。

qiita.rbvar
1.let optionalString:String? = "a"
2.if String = optionalString ?? "b"

//実行結果 a

 強制アンラップとは?

強制アンラップは、Optional(Wrapped)型からWrapped型の値を強制的に取り出す方法です。
強制アンラップを行うには、!演算子を使用します。
上記では、Wrapped型の変数や定数と同じように扱うことができないと述べましたが、強制アンラップ使うことによって取り出すことができます。

qiita.rbvar
let a : Int? = 1
let b : Int? = 1
a!+b! = //2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ios/firebase】初めてのtwitter認証

やりたきこと

firebaseを使ったiosアプリでtwitter認証してみる

誰向け?

つい数時間前の自分
 ・WEBバックエンド開発を主な生業
 ・iosアプリ開発は未経験
 ・twitter利用経験なし(アカウントはさっき作った)
 ・公式ドキュメントの手順見ても、どこに追記するのかすら分からん

そんな数時間前の自分に向けてやさしく解説してみる。

環境

Xcode Version 10.1
CocoaPods Version 1.9.3
Swift Version 4.2.1

早速開発

公式 「はじめに」 -> 1 の設定
Firebase/Authをinstall

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

  # Pods for app
  pod 'Firebase/Auth'  <---追加
end
pod update

FirebaseのAuthenticationのtwitter認証の設定を行う

公式 「はじめに」 -> 2,3,4 の設定
スクリーンショット 2020-10-09 7.09.35.png

URL Typesの設定

公式 「Firebase iOS SDK でログインフローを処理するには:」 -> 1の設定

REVERSED_CLIENT_IDの情報を控える
スクリーンショット 2020-10-09 7.19.14.png

URL TypesのURL Schemesに追加
スクリーンショット 2020-10-09 8.50.33.png

実装

「twitterでSign in」ボタンだけ配置して認証/認可を確認
スクリーンショット 2020-10-09 9.05.45.png

実装内容は公式丸パクリ!

ViewController.swift
import UIKit
import Firebase

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func twitterSignInButton(_ sender: Any) {
        var provider = OAuthProvider(providerID: "twitter.com")
        provider.getCredentialWith(nil) { credential, error in
            if error != nil {
                // Handle error.
            }
            if credential != nil {
                Auth().signIn(with: credential) { authResult, error in
                    if error != nil {
                        // Handle error.
                    }
                     // User is signed in.
                }
            }
        }
    }
}

・・・すると、ビルドエラーになります

スクリーンショット 2020-10-09 5.37.18.png

この記事を参考に解決

diff
 import UIKit
 import Firebase
+import FirebaseAuth

 class ViewController: UIViewController {

+    var twitterProvider : OAuthProvider?
+    
     override func viewDidLoad() {
         super.viewDidLoad()
+        self.twitterProvider = OAuthProvider(providerID:"twitter.com");
     }

    @IBAction func twitterSignInButton(_ sender: Any) {
-        var provider = OAuthProvider(providerID: "twitter.com")
-        provider.getCredentialWith(nil) { credential, error in
    @IBAction func twitterSignInButton(_ sender: Any) {
+        self.twitterProvider?.getCredentialWith(_: nil){ (credential, error) in
             if error != nil {
                 // Handle error.
             }
-            if credential != nil {
-                Auth().signIn(with: credential) { authResult, error in
+            if let credential = credential {
+                Auth.auth().signIn(with: credential) { (authResult, error) in
                     if error != nil {
                         // Handle error.
                     }

とりあえず、動かす
スクリーンショット 2020-10-09 6.00.19.png

動いた!

動作確認

「twitter でSign in」ボタンを押すと
スクリーンショット 2020-10-09 6.52.36.png

Sigin Inしても何も実装していないので、最初の画面が出てきちゃいます。
認証されたかはFirebaseの画面で確認
スクリーンショット 2020-10-09 6.53.15.png

大丈夫そう!

最後に

以前、WEBサービスのtwitter認証周りをやったことがあり、
結構大変だった記憶があるのですが、Firebase使うと簡単にできました。
小規模アプリなら活用する機会多いかもしれないです。
これからアプリのほう拡張していきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlin Multiplatform Mobile がアルファ段階に移行したので、チュートリアルを試す

背景

最近、Kotlin Multiplatform Mobile(KMM)がアルファ段階に移行したようです。

Kotlin Multiplatform Mobile がアルファ段階に移行 – Kotlin Blog | JetBrains

KMM とは、JetBrains が提供するクロスプラットフォーム対応のモバイル開発用 SDK です。
Kotlin のマルチプラットフォーム対応能力を駆使し、モバイルアプリケーションの構築体験を可能な限り楽しく効率的にするように設計されたさまざまなツールや機能を含んでいます。

見ていて面白そうだったので、試しにチュートリアルをやってみました。
※2020 年 10 月時点の情報なので、今後のアップデートにより変わる可能性があります。

開発環境

開発環境は以下の通りです。

  • Android Studio 4.1 RC 3 以降
  • Xcode 11.3 以降
  • Kotlin 1.4.0 以降
  • Java 1.8.0_73 以降
  • macOS Catalina バージョン 10.15.6

環境構築

以下をベースに進めていきます。

Getting started - Help | Kotlin Multiplatform Mobile Docs

Android Studio version 4.1 RC 3 以降をインストール

Android Studio Previewから、4.1 RC 3 か 4.2 CANARY 13 のいずれかをインストールします。
どちらでも大丈夫です。

1.png

Kotlin のバージョンを 1.4.0 以降にアップデート

Configure->Pluginsから、Kotlin を 1.4.0 以降にアップデートします。

2.png

Kotlin Multiplatform Mobile をインストール

Configure->Pluginsから、Kotlin Multiplatform Mobile をインストールします。

3.png

インストール後、Android Studio を再起動します。

はじめてのマルチプラットフォームアプリ作成

以下をベースに進めていきます。

Create your first multiplatform application - Help | Kotlin Multiplatform Mobile Docs

プロジェクト作成

Create New Projectから新規でプロジェクトを作成します。

4.png

Select a Project Templateより、KMM Applicationを選択し、Nextをクリックします。

5.png

各項目を以下の通り入力します。

項目 名称
Name KMM Application
Package name com.example.kmmapplication
Save location 任意のフォルダ

6.png

Configure ActivityではデフォルトのままFinishをクリックします。

7.png

ビルド

早速ビルドしてみます。
まずは Android から。

androidAppを選択してから、ビルド対象の端末を設定してビルドします。

8.png

すると、以下のように起動します。

9.png

次に iOS を試してみます。
iosAppを選択し、Edit Confiugrations...をクリックします。

10.png

Execution targetで端末を設定し、ビルドします。

11.png

すると、以下のように起動します。

12.png

テストコード

各プラットフォームごとにテストコードが用意されており、それぞれ以下のようにファイルを開いて実行できます。

13.png

Android のほうは、shared/src/androidTest/kotlin/com.example.kmmapplication.sharedandroidTest.ktにあります。

package com.example.kmmapplication.shared

import org.junit.Assert.assertTrue
import org.junit.Test

class AndroidGreetingTest {

    @Test
    fun testExample() {
        assertTrue("Check Android is mentioned", Greeting().greeting().contains("Android"))
    }
}

iOS のほうは、shared/src/iosTest/kotlin/com.example.kmmapplication.sharediosTest.ktにあります。

package com.example.kmmapplication.shared

import kotlin.test.Test
import kotlin.test.assertTrue

class IosGreetingTest {

    @Test
    fun testExample() {
        assertTrue(Greeting().greeting().contains("iOS"), "Check iOS is mentioned")
    }
}

どちらもGreeting().greeting()が出力する文字列にプラットフォーム名が含まれているかのテストをしています。
shared/src/commonTest/kotlinという共通用のテストフォルダも用意されているようですが、デフォルトでは何も入っていませんでした。

アプリケーションの更新

画面上では「Hello, [プラットフォーム名とバージョン]」という文字列が表示されていました。この文字列を生成しているクラスがshared/src/commonMain/kotlin/com.example.kmmapplication.sharedGreeting.ktにあります。

package com.example.kmmapplication.shared


class Greeting {
    fun greeting(): String {
        return "Hello, ${Platform().platform}!"
    }
}

このディレクトリには、Android と iOS の両方のプラットフォームの共有コードが保存されます。共有コードに変更を加えると、両方のアプリケーションに変更が表示されます。
試しに以下のように変更してみます。

class Greeting {
    fun greeting(): String {
        return "Guess what it is! > ${Platform().platform.reversed()}!"
    }
}

変更後、各プラットフォームでビルドしてみます。
いずれも変更が反映されていることを確認しました。

Android iOS
14.png 15.png

最後に、Platform クラスについても確認してみます。
commonMain/kotlin/com.example.kmmapplication.sharedPlatform.ktです。

package com.example.kmmapplication.shared

expect class Platform() {
    val platform: String
}

expectキーワードはマルチプラットフォーム専用のインタフェースにて使われるキーワードで、あるプラットフォームのクラスを使いたい場合に使うキーワードです。
androidMainiosMainにもそれぞれPlatform.ktがあります。

Android のほうは、androidMain/kotlin/com.example.kmmapplication.sharedPlatform.ktです。

package com.example.kmmapplication.shared

actual class Platform actual constructor() {
    actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

iOS のほうは、iosMain/kotlin/com.example.kmmapplication.sharedPlatform.ktです。

package com.example.kmmapplication.shared


import platform.UIKit.UIDevice

actual class Platform actual constructor() {
    actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

それぞれactualキーワードが使われています。プラットフォームごとに、expectクラスをactualで実装する必要があります。
interfaceとそれをimplementするclassの関係に似ています。

まとめ

Kotlin Multiplatform Mobile(KMM)がアルファ段階に移行したことを受けて、試しにチュートリアルをやってみました。
どうやら共通化するのはロジックのみで、UI は各プラットフォームごとに独自で実装する必要がありそうです。
さらに以下のようなハンズオンも用意されているので、時間があるときにまた試してみます。

Hands-on: Networking and Data storage - Help | Kotlin Multiplatform Mobile Docs

参考 URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む