しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

【Swift】isとisMemberOfClassとisKindOfClass

isとisMemberOfClassとisKindOfClassですが、挙動の差がわかりにくかったのでまとめてみました。

isKindOfClass

下のようなケースでは当然trueになります。
それとisKindOfClass/isMemberOfClassはNSObjectProtocolのメソッドなので、NSObjectなどを継承しているクラスでのみ使えます。

class MyClass: NSObject {}

let myClass = MyClass()
print(myClass.isKind(of: MyClass.self)) // → true

isKindOfClassは判定対象が引数のサブクラスの場合もtrueになります。

class MyClass: NSObject {}
class MySubClass: MyClass {}

let myClass = MySubClass()
print(myClass.isKind(of: MyClass.self)) // → true

isMemberOfClass

isMemberOfClassはisKindOfClassと違って、サブクラスとの判定はfalseになります。

class MyClass: NSObject {}
class MySubClass: MyClass {}

print(MyClass().isKind(of: MyClass.self)) // → true
print(MySubClass().isKind(of: MyClass.self)) // → false

is

isはisKindOfClass同様、サブクラスとの判定もtrueになります。
isKindOfClassと違い、NSObjectProtocolに準拠していないクラスでも使えます。

class MyClass {}
class MySubClass: MyClass {}

print(MyClass() is MyClass) // → true
print(MySubClass() is MyClass) // → true

isMemberOfClass相当の判定方法

NSObjectProtocolに準拠してないクラスでisMemberOfClass同様の判定をしたい場合は、下の方法が使えそうです。
String(describing: type(of: myClass))と"(MyClass.self)"はクラス名を返すので、サブクラスとの比較がfalseになります。

class MyClass {}
class MySubClass: MyClass {}

let myClass = MyClass()
let mySubClass = MySubClass()
print(String(describing: type(of: myClass)) == "\(MyClass.self)")
print(String(describing: type(of: mySubClass)) == "\(MyClass.self)")

文字列での判定なので、下のように別フレームワークと同じクラス名を使ってる場合は注意が必要です。

class UIView {}

print(UIView() is UIKit.UIView) // → false
print(String(describing: type(of: UIView())) == "\(UIKit.UIView.self)") → true

【Swift】複数のプロトコルに準拠した変数の定義

下で定義された2つのプロトコルに準拠する型の書き方です。

protocol MyProtocol1 {}
protocol MyProtocol2 {}

複数のプロトコルへの準拠を表したい時は、下のように&を使います。

protocol MyProtocol1 {}
protocol MyProtocol2 {}
class MyClass: MyProtocol1, MyProtocol2 {}

let value: (MyProtocol1 & MyProtocol2) = MyClass()

下のように、引数・返り値に使う事もできます。

protocol MyProtocol1 {}
protocol MyProtocol2 {}
class MyClass: MyProtocol1, MyProtocol2 {}

class TestClass {
    var value1: (MyProtocol1 & MyProtocol2)?
    var value2: (MyProtocol1 & MyProtocol2) {
        return MyClass()
    }
    
    func myMethod(value: (MyProtocol1 & MyProtocol2)) -> (MyProtocol1 & MyProtocol2) {
        return MyClass()
    }
}

上のように何回も使う場合、typealiasで再定義するとすっきりします。

protocol MyProtocol1 {}
protocol MyProtocol2 {}
class MyClass: MyProtocol1, MyProtocol2 {}
typealias MyProtocols = MyProtocol1 & MyProtocol2

let value: MyProtocols = MyClass()

Amper Musicで簡単BGM作成

Amper Musicというサービスを試してみました。
これは音楽を生成してくれるサービスで、30秒ほどの曲を簡単に作る事ができます。

www.ampermusic.com

ライセンスは、非商用なら無制限に利用可能。
商用利用したい時は問い合わせが必要です。

Legal | Amper Music® - AI Music Composer for Content Creators

まずは会員登録をします。

f:id:llcc:20171013211533p:plain

すると次のような画面になるので、CreateNewProjectを選択します。

f:id:llcc:20171013211612p:plain

タイプはSimpleを選択しました。

f:id:llcc:20171013211642p:plain

次は曲の種類を選んでいきます。

f:id:llcc:20171013211706p:plain

f:id:llcc:20171013211715p:plain

最後に曲の長さを選択して完了です。

f:id:llcc:20171013211753p:plain

f:id:llcc:20171013211820p:plain

作成した曲はmp3かwavで出力する事ができます。

f:id:llcc:20171013212238p:plain

Universal Linksを試してみる

iOSから登場したUniversal Linksを試してみました。

Universal Linksとは

Universal Linksとは特定のURLにアクセスした時にアプリを遷移させる事ができる技術です。
例えば「https://example.com/にアクセスした時にアプリAに遷移する」と言った挙動が可能です。

アプリがインストールされていない場合はhttps://example.com/のWEBページが表示されます。

サーバーサイドの準備

まずはユニバーサルリンクに対応させたいページのサーバー側にユニバーサルリンク用のファイルをアップロードします。
apple-app-site-associationという名前のファイルを作ってルートフォルダに配置してください。

ファイルの中身は下の通りです。
appIDにはteam idとbundle idを.で連結したものをセットします。

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID":"XXXX(team id).XXXXXX(bundle id)",
        "paths":[ "*" ]
      }
    ]
  }
}

Team IDはMember Centerから確認する事ができます。

f:id:llcc:20171008140333p:plain

アプリ側の準備

続けてアプリ側の設定をします。
XcodeのCapabilityからassociated-domainsの有効化をして、applinks:XXX(ドメイン)という値をセットします。

f:id:llcc:20171008143754p:plain

続けてMemberCenterのApp IDsからもAssociated Domainsを有効にします。

f:id:llcc:20171008144408p:plain

これで対応は完了です。
アプリを起動後に、該当ページへアクセスすればアプリがシームレスに開きます。

ARKitでタップした場所に立方体を配置する

iOS11で登場したARKitを使って、画面上に立方体を配置してみました。
下のように地面に複数のオブジェクトが並ぶようにしています。

f:id:llcc:20171006144302p:plain

まずはプロジェクト作成から「Augmented Reality App」を選んでARKitのテンプレートを作成します。

f:id:llcc:20171006144342p:plain

次はサンプルコードを消してコードを最小限にします。

import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate {
    @IBOutlet var sceneView: ARSCNView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        sceneView.delegate = self
        sceneView.showsStatistics = true
        let scene = SCNScene()
        sceneView.scene = scene
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        let configuration = ARWorldTrackingConfiguration()
        sceneView.session.run(configuration)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        sceneView.session.pause()
    }
}

次はデバック用オプションを指定します。
このオプションは認識した特徴点を表示してくれるものです。
特徴点が一定数を超えるとその場所を平面としてみなしてくれます。

class ViewController: UIViewController, ARSCNViewDelegate {
    @IBOutlet var sceneView: ARSCNView!
    
    override func viewDidLoad() {
        // 省略
        
        sceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints
    }

    // 省略
}

実行すると画面に特徴点が黄色い点として現れます。

f:id:llcc:20171006144658p:plain

次は平面認識をする設定をします。
ARWorldTrackingConfigurationのplaneDetectionに.horizontalをセットします。
今は.horizontalしかないので、平面以外の認識はできません。

class ViewController: UIViewController, ARSCNViewDelegate {
    // 省略
        
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = .horizontal // 今回追加
        sceneView.session.run(configuration)
    }
}

最後にタップ時にボックスを配置する処理を記述します。
下のようにUITapGestureRecognizerのセットと、タップ時に呼ばれるメソッドを実装します。

class ViewController: UIViewController, ARSCNViewDelegate {
    @IBOutlet var sceneView: ARSCNView!
    
    override func viewDidLoad() {
        // 省略
        
        let gesture = UITapGestureRecognizer(target: self, action: #selector(tapView))
        sceneView.addGestureRecognizer(gesture)
    }

    @objc func tapView(sender: UITapGestureRecognizer) {
        let location = sender.location(in: sceneView)
        
        let hitTestResult = sceneView.hitTest(location, types: .existingPlane)
        if let result = hitTestResult.first {
            let geometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
            let material = SCNMaterial()
            material.diffuse.contents = UIColor.darkGray
            geometry.materials = [material]
            
            let node = SCNNode(geometry: geometry)
            node.position = SCNVector3(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y + 0.20, result.worldTransform.columns.3.z)
            sceneView.scene.rootNode.addChildNode(node)
        }
    }
    
    // 省略
}

場所の取得は下メソッドで行っています。
locationメソッドで画面上のタップ位置を取得して、hitTestでタップ位置と認識済平面との交点の座標を計算しています。

取得後は、普通のSceneKitアプリ同様にボックスを配置すれば完成です。

let location = sender.location(in: sceneView)
let hitTestResult = sceneView.hitTest(location, types: .existingPlane)

f:id:llcc:20171006144302p:plain

参考URL

[iOS 11][ARKit] 物理衝突を実装して、キューブを落として見る #WWDC2017 | Developers.IO

Codable+AlamofireでAPIのレスポンスをオブジェクトに変換、Swift4.1対応版

AlamofireとCodableを使った実装を試してみました。
Alamofireは事前にCocoaPodsでインストールしておきます。

Alamofire+Codableの事前準備

target 'MyApp' do
  use_frameworks!

  pod 'Alamofire'
end

もし非SSL(http)通信をする場合はApp Transport Security Settingsの設定を事前にしておいてください。

f:id:llcc:20180623224058p:plain

Alamofire+Codableの簡単な使い方

まずは下のような簡単な構造のデータをAPI経由で受け取ってみます。

{
    id: 1,
    name: 'my_name'
}

最初にCodableに準拠した構造体を作ります。

struct User: Codable {
    let id: Int
    let name: String
}

構造体を作ったらサーバーからのレスポンスを構造体に変換する処理を実装します。
実装は下の通りです。

この例ではgetでuserのデータを取得しています。

Alamofire.request("http://localhost:3000/users/1.json").response { response in
    if let data = response.data {
        let result = try? JSONDecoder().decode(User.self, from: data)
        print(result)
    }
}

ログを見ると無事にデータが取得できている事が分かります。

f:id:llcc:20180623224310p:plain

Codableの型が違う場合

下のようにresponseと構造体の型が違う場合の挙動も見てみます。

struct User: Codable {
    let id: Int
    let name1: String // name → name1に変更
}

下のようにtry?を使っている場合はnilが返ってきます。

let result = try? JSONDecoder().decode(User.self, from: data) // → nil

try!を使っている場合はアプリが落ちます。

let result = try! JSONDecoder().decode(User.self, from: data) // → クラッシュ

エラーメッセージは下の通りです。

Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "name1", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"name1\", intValue: nil) (\"name1\").", underlyingError: nil)): file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-902.0.54/src/swift/stdlib/public/core/ErrorType.swift, line 184
2018-06-23 22:45:49.750172+0900 MyApp[19279:6248046] Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "name1", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"name1\", intValue: nil) (\"name1\").", underlyingError: nil)): file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-902.0.54/src/swift/stdlib/public/core/ErrorType.swift, line 184

少し話は逸れますがレスポンスのキーが多い分にはエラーは起きません。

# レスポンス
{
  id: 1,
  name: 'my_name',
  name2: 'my_name',
  name3: 'my_name',
  name4: 'my_name',
}
let result = try? JSONDecoder().decode(User.self, from: data) // → 問題なくデコードできる

Codableで数字をDouble型として扱う

数値型はIntではなくDoubleで受け取る事もできます。
下のように変えるだけで問題なくデコードします。

struct User: Codable {
    let id: Double
    let name: String
}

ただし数値を文字列にする事はできません。

struct User: Codable {
    let id: String
    let name: String
}

Codableで複数オブジェクトをパースする

下のように複数データが返ってくる場合について見ていきます。

[{
  id: 1,
  name: 'my_name',
},{
  id: 2,
  name: 'my_name2',
},{
  id: 3,
  name: 'my_name3',
}]

複数のデータが返る時はUser.selfをArray.selfに置き換えるだけです。

Alamofire.request("http://localhost:3000/users.json").response { response in
    if let data = response.data {
        let result = try? JSONDecoder().decode(Array<User>.self, from: data) // User.selfをArray<User>.selfに置き換えただけ
        print(result)
    }
}

ログを見るとしっかりデコードされている事が分かります。

f:id:llcc:20180623225352p:plain

Codableでスネークケースのデータを受け取る

次のようにサーバーからのレスポンスはスネークケース、iOSアプリはキャメルケースの場合も見ていきます。

# レスポンスはスネークケース
{
  id: 1,
  name: 'my_name',
  book_count: 1
}
// 構造体はキャメルケース
struct User: Codable {
    let id: Int
    let name: String
    let bookCount: Int
}

こういった場合はJSONDecoderのkeyDecodingStrategyオプションを利用します。
keyDecodingStrategyに.convertFromSnakeCaseをセットする事でスネークケースとキャメルケースの変換をしてくれるようになります。

Alamofire.request("http://localhost:3000/users/1.json").response { response in
    if let data = response.data {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase // スネークケースとキャメルケースの変換をするオプション
        let result = try? decoder.decode(User.self, from: data)
        print(result)
    }
}

ログを見ると無事に変換できている事が分かります。

f:id:llcc:20180623230407p:plain

keyDecodingStrategyですがSwift4.1から登場したのでそれ以前のSwiftでは使えません。

CodableでDate型を扱う

下のようにサーバーがDate型のプロパティーを返す時も確認します。

{
  id: 1,
  name: 'my_name',
  created_at: '2018-01-01T00:00:00.000Z',
  updated_at: '2018-01-01T00:00:00.000Z'
}

まずはレスポンスに合った構造体を作ります。

struct User: Codable {
    let id: Int
    let name: String
    let createdAt: Date
    let updatedAt: Date
}

デコード処理は下の通りです。
JSONDecoderのdateDecodingStrategyというプロパティーにDateの形式をセットします。

Alamofire.request("http://localhost:3000/users/1.json").response { response in
    if let data = response.data {
        let decoder = JSONDecoder()
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
        decoder.dateDecodingStrategy = .formatted(formatter)
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        let result = try? decoder.decode(User.self, from: data)
        print(result)
    }
}

ログを見るとDateもしっかりと変換できている事が分かります。

f:id:llcc:20180624150506p:plain

Codableでネスト(入れ子)のデータを扱う

最後に下のようなネスト形式について見ていきます。

{
  id: 1,
  name: 'my_name',
  books: [
    { id: 1, title: 'title1' },
    { id: 2, title: 'title2' },
  ]
}

上のようなデータを受け取りたい場合は下のような構造体を作ります。

struct User: Codable {
    let id: Int
    let name: String
    let books: [Book]
}

struct Book: Codable {
    let id: Int
    let title: String
}

デコード部分は今までと同じ処理で問題ありません。

Alamofire.request("http://localhost:3000/users/1.json").response { response in
    if let data = response.data {
        let result = try? JSONDecoder().decode(User.self, from: data)
        print(result)
    }
}

実行してログを見るとUserのbooksプロパティーに値が入っている事が分かります。

f:id:llcc:20180624145316p:plain

まとめ

Swift4から登場したCodableですがかゆいところに手が届く感じでとても便利そうでした。
今まではObjectMapperがメインだったんですが今後はCodableも使ってみようと思います。

SSReadingListでリーディングリストに項目追加

SafariServicesのSSReadingListという機能を使ってiPhoneのリーディングリストに項目を追加してみました。

リーディングリストとは

Safariについている機能で、サイトを保存してあとで見る事ができます。
ブックマークと近いのですが「オフラインでも見れる」「PCでも共有できる」といった特徴があります。

Safariからの追加は下のメニューで行います。

f:id:llcc:20171005143340p:plain

同じAppleIDを使っている場合、MacとiPhoneでサイトの共有もできます。

f:id:llcc:20171005143405p:plain

リーディングリストの一覧画面からオフラインで保存する事もできます。

f:id:llcc:20171005143519p:plain

保存しておけば機内モードでもサイトを閲覧する事ができます。

f:id:llcc:20171005143800p:plain

今回はSSReadingListを使ってリーディングリストへの追加をしてみます。

SSReadingListを使った保存

セットは下のように行います。
SSReadingListのaddItemを呼ぶことで保存しています。

import UIKit
import SafariServices

class ViewController: UIViewController {
    var session: SFAuthenticationSession!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let url = URL(string: "http://www.cl9.info/") {
            try? SSReadingList.default()?.addItem(with: url, title: "しめ鯖日記", previewText: "技術ブログ")
        }
    }
}

保存されたリーディングリストは下のように表示されます。

f:id:llcc:20171005144300p:plain

オフライン用に保存されるかどうかは、iPhoneの設定によります。
この設定はこちらでコントロールすることはできないようです。

f:id:llcc:20171005144540p:plain

自動保存が適用されている場合、サムネイルやサイト説明はサイト上のものが利用されます。

f:id:llcc:20171005144704p:plain

リーディングリストの取得

取得についてはiOS11時点ではできません。
今後APIが開放される事を願います。