Type "xxx" does not confirm to protocol 'NSObjectProtocol'エラーの対処法

はじめに

タイトル通りのエラーが出た時の対処法です。

例えば、Objective-Cのライブラリで、以下のようなプロトコルがあり、

@protocol Hogehoge <NSObject>

これを宣言しようとした際、

class Fuga: Hogehoge

以下のようなエラーが出ます.

Type "Fuga" does not confirm to protocol 'NSObjectProtocol'

これは、Hogehoge自体がNSObjectを継承したものなので、Fugaにその定義が無いとエラーを起こしているのですが、XCodeの補助機能で、足りていない定義を追加しようとした場合、下記のようになってしまいます。

    func isEqual(_ object: Any?) -> Bool {
        <#code#>
    }
    
    var hash: Int
    
    var superclass: AnyClass?
    
    func `self`() -> Self {
        <#code#>
    }
    
    func perform(_ aSelector: Selector!) -> Unmanaged<AnyObject>! {
        <#code#>
    }
    
    func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged<AnyObject>! {
        <#code#>
    }
    
    func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged<AnyObject>! {
        <#code#>
    }
    
    func isProxy() -> Bool {
        <#code#>
    }
    
    func isKind(of aClass: AnyClass) -> Bool {
        <#code#>
    }
    
    func isMember(of aClass: AnyClass) -> Bool {
        <#code#>
    }
    
    func conforms(to aProtocol: Protocol) -> Bool {
        <#code#>
    }
    
    func responds(to aSelector: Selector!) -> Bool {
        <#code#>
    }
    
    var description: String

解決方法

これを埋めても良いのですが・・・、
単純にプロトコルを継承するだけなのと、NSObjectが使えれば良いだけなので、

class Fuga: NSObject, Hogehoge

と、NSObjectを継承しておけば解決します。

久々にiOS開発してここで詰まってしまった為、備忘録的な記事でした。

FirebaseUIの認証を使ってドハマリした件 - Cannot read property 'length' of undefined Dismiss

はじめに

Project.Unknown内で利用するツールをFirebaseで行おうと考え、PJ内だけで利用したいため、折角なのでFirebase Authenticationを利用して認証されたメンバーだけ閲覧できるページを作ろうと、以下のモジュールをFirebaseUIで実装しました。

f:id:project-unknown:20190528105232p:plain

この時にドハマリしたのが、Sign inしようとしたら、以下のエラーになった件です。

f:id:project-unknown:20190528105007p:plain

Cannot read property 'length' of undefined Dismiss

処理云々はFirebaseUIにまかせているので、何故こうなったのかさっぱりわからず、丸1日消費してしまいました。

解決法

いろいろ模索したのですが、firebase.jsを最新にする事で解決することができました。

before

<script src="https://www.gstatic.com/firebasejs/5.9.1/firebase.js"></script>

after

<script src="https://www.gstatic.com/firebasejs/6.0.2/firebase.js"></script>

それ以外だと、2019/5/28現在、以下のFirebaseUIを参照しています。

<script src="https://cdn.firebase.com/libs/firebaseui/4.0.0/firebaseui.js"></script>
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/4.0.0/firebaseui.css" />

また、最新のSDKの情報は以下から確認できます。

firebase.google.com

github.com

iOSでインジケータのようなくるくる回るアニメーションを造る - CGAffineTransform

はじめに

インジケータを自作する方法です。
標準で搭載されているものでも十分事足りるのですが、アプリの色を出したいときにインジケータの演出もこだわりたいですよね。

提供中のTIME HACKERにも自作のインジケータを導入した方ので、以下のキャプチャのようなものを自作しました。

f:id:project-unknown:20181103200007g:plain

今日は、その際のやりかたです。
パラパラ漫画のような作り方でも良いのですが、上記キャプチャを見てもらえばわかるように、一部だけエンドレスに回転させる程度なら、CGAffineTransformを利用します。

CGAffineTransform

CGAffineTransformは、UIViewの拡大・縮小・回転などを実装するのをサポートしています。

これを用いて、回転するUIViewを作ります。

class IndicatorView: UIView {

    @IBOutlet weak var indicatorPin: UIImageView!

    func start() {
        
            UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveLinear, animations: {
                self.indicatorPin.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
                self.indicatorPin.transform = CGAffineTransform.identity
            }, completion: { completed in
                self.start(title: title)
            })
        }
}

indicatorPinに回転したい画像をセットし、startメソッド内の、UIView.animateメソッドを使って回転させます。

animationsの中身は、以下のように指定することで、1周分の回転を指定します。

self.indicatorPin.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
self.indicatorPin.transform = CGAffineTransform.identity

最後のcompletionで自分自身を再帰呼び出し、1周回ったら次の周へ・・・とエンドレスに呼び出しています。

また、途中で止めたい場合は、stop用のメソッドなどを作ると良いかもです。

完成形

上記含めて、自作Indicatorの簡単な完成形です。
outletのところは、xibと接続してください。

class IndicatorView: UIView {

    
    /// Indicatorの動作フラグです
    ///  - true: Indicatorの動作を停止します
    ///  - false: Indicatorの動作を開始します
    var finished = true
    
    @IBOutlet weak var indicatorPin: UIImageView!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }

    
    /// Indicatorを開始します
    ///
    func start() {
        finished = false
        running()
    }
    
    
    /// Indicator animationを実行します
    func running() {
        if finished == true {
            return
        }
        
        UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveLinear, animations: {
            self.indicatorPin.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
            self.indicatorPin.transform = CGAffineTransform.identity
        }, completion: { completed in
            self.running()
        })
    }
    
    func stop() {
        finished = true
    }

    private func loadNib(){
        let view = Bundle.main.loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
    }
}

最後に、これを使いたいViewControllerのStoryboard等に設置して、以下のように呼び出すだけです。

{IndicatorViewのインスタンス}.start()
{IndicatorViewのインスタンス}.stop()

XCode10でビルドできなくなった時にやったこと - Multiple commands produce error

XCode10がリリースされ、早速手元にあるXCode9でビルドしたアプリもXCode10用にビルドしようとした際に以下のエラーが発生しました。

error: Multiple commands produce '/Users/{アカウント名}/Library/Developer/Xcode/DerivedData/{アプリ名}-hievebmhhbbmlybyrcuahkuwpdgk/Build/Products/Debug-iphonesimulator/{アプリ名}.app':
1) Target '{アプリ名}' has create directory command with output '/Users/{アカウント名}/Library/Developer/Xcode/DerivedData/{アプリ名}-hievebmhhbbmlybyrcuahkuwpdgk/Build/Products/Debug-iphonesimulator/{アプリ名}.app'
2) That command depends on command in Target '{アプリ名}': script phase “[CP] Copy Pods Resources”

原因は、XCodeのbuildシステムのデフォルトが変更された事によります。
buildシステムについては、以下から確認できます。

f:id:project-unknown:20180923190113p:plain

f:id:project-unknown:20180923190145p:plain

上記のように、XCode10からは、New Build Systemがデフォルトになっています。
(XCode9までは、Legacy Build Systemに該当するものがこれに当たります)

これに対する対処法は、2通りあります。

  1. Product Nameで${TARGET_NAME}を使うのをやめる
  2. Legacy Build Systemを採用する

結論を言うと、私は今回の対応で2を選択しました。
今回は、それぞれの対応方法についでご紹介します。

対応方法1 Product Nameで${TARGET_NAME}を使うのをやめる

おそらくこれが推奨されるやり方です。

Build Settings -> Product Name

ここに${TARGET_NAME}で設定されているものを、環境変数を使うのではなくて、手打ちでProduct Nameを入力します。

f:id:project-unknown:20180923190543p:plain

今回、私がやったのはTimeHackerアプリの設定を行ったので、「TimeHacker」と入力しました。

後は、WidgetやWatchでも同様の事を実施して、Buildするだけです。

…が、私の場合、おそらくCocoaPodsのパッケージ依存か何かでうまく動きませんでした。

対応方法2 Legacy Build Systemを採用する

冒頭で紹介したWorkspace Settings...の設定をLegacy Build Systemに変更します。
これでこれまでのBuild(XCode9までのBuild)が実行されます。

f:id:project-unknown:20180923190113p:plain

f:id:project-unknown:20180923190850p:plain

最後に

New Build SystemはSwift製と言うのと、今後、こちらをデファクトスタンダードになっていくと思われます。
Podsやパッケージが対応されたら、New Build Systemにうつしていこうと思います。

Apple Watch - Watch OS4 でCoreData等を用いた開発 - Swift4

Apple Watch (Watch OS4)

この記事では、Apple Watch (以降Watch OS)の開発にあたっての私なりの開発メモを記します。
また、何の断りがなければ、Watch OSはWatch OS4の事を指しています。

環境としては、

  • XCode 9.4.x
  • Watch OS 4 (Watch Kit2)

での開発です。
特にCoreData等のリソース周りの資料があまり見つからなかったので、そこを重点的に記載しています。

また、今回の記事はTIME HACKERでApple Watch対応を行っている時に、主に私が悩んだ所ですので、CoreDataとのデータのやり取りを主にフォーカスしています。

これまでのWatch OS (初代Watch Kit)との違い

Watch OS開発は、これまで(初代Watch Kit)とは、特にリソースの取扱方法が異なってます。

その違いは、以下のAppleが展開している図を見ると明らかです。

f:id:project-unknown:20180902001023p:plain

これまでは、Apple WatchはiPhone上で動作させること前提の仕組みとなっていた為、CoreDataやUserDefaultsをApp Groupsの設定さえ行っていれば、共有リソースとしてアクセスすることが出来ました。

しかし、Watch Kit 2からは、Apple Watch上で独立して動作する事になり、
Apple Watchから共有リソースへのアクセスができなくなりました。

新しいWatch OSでのCoreData等のリソースアクセスの考え方 Watch Connectivity

UserDefaults等で取り扱う、一時的な設定等は、Apple Watchでそのまま使えば良いでしょう。
しかし、iPhone上のアプリとデータを共有する場合は、Apple Watch上のアプリからiPhoneアプリへデータの送受信を行うと言う考え方で取り扱う事になります。
このときに、Watch Connectivityを用いて送受信を行います。

図で言う、枠で囲った所がそれに当たります。

f:id:project-unknown:20180902001752p:plain

しかし、これには注意点があります。

これまでは単純に同一デバイス内のリソースファイルへアクセスしていたので、それなりに高速に取り扱うことが出来ましたが。
Apple WatchとiPhoneとは物理的にも分断されている通り、Sessionでのデータのやり取りになります。
当然、画像ファイル等のやり取りを行うと、その分データの送受信に時間がかかるので注意が必要です

画像ファイルをWebから取得して利用するのではなく、予めアプリに組み込んだものを利用するのであれば、iPhoneとApple Watch両方に同一画像を埋め込んでおくのが妥当だと思います。

では、実際にiPhoneとWatch OSとでリソースのやり取りを行うサンプルを造っていきます。

データ送受信部分

まずは、データの送受信を行う所を造ります。
イメージは、繰り返しになりますが、以下の図の用に考えます。

f:id:project-unknown:20180902003023p:plain

iPhone, Watch OS共にデータの送受信を行う為のSessionをこさえます。

iPhone App側の実装

以下のようにWatchManagerクラスを用意します。

WatchManagerは以下の役割を担います。

  • Watch OSとのSession
  • Watch OSから来たEventの種別を判定 (後述)

また、WatchManagerはSessionを取り扱う為、念の為シングルトン設計にしています。

import WatchConnectivity

class WatchManager: NSObject, WCSessionDelegate {

    let wcSession = WCSession.default
    
    private static let instance = WatchManager()
    
    public class var shared: WatchManager {
        
        return instance
    }
    

    func initWCSession() {
        if WCSession.isSupported() {
            wcSession.delegate = self
            wcSession.activate()
            print("[INFO] session activate")
        } else {
            print("[INFO] session error")
        }
    }

}

extension WatchManager {
    
    /// メッセージを受信した時の処理
    ///
    /// - Parameters:
    ///   - session:
    ///   - message:
    ///   - replyHandler:
    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        
        var result: [String: Any] = ["success" : false]
        replyHandler(result)
        
    }
    
}

/// Session Delegate
extension WatchManager {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("[INFO] The session has completed activation.")
    }
    
    func sessionDidBecomeInactive(_ session: WCSession) {
        print("[INFO] The session has got into inactivation.")
    }
    
    func sessionDidDeactivate(_ session: WCSession) {
        print("[INFO] The session has deactivated")
    }
    
    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        print(userInfo)
    }
    
    func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
        print("[INFO] try to send message to watch")
        sendMessage()
    }
}

やっていることは、WCSessionオブジェクトを生成し、delegateを自分自身にしているだけです。
これを例えば、AppDelegate等で

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        WatchManager.shared.initWCSession()
    }
}

と、呼んであげるとWatch OS用のSessionを貼ることが出来ます。
後は、Watch OSからEventが飛んでくると、適宜Watch Manager内のDelegateが呼び出されます。

上記サンプルで

    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    }

このコードだけ、中身に軽い処理を入れているのは、今回のサンプルでは、主にこのDelegateを用いる為です。念の為。

Watch OS側の実装

iPhone App側の実装は出来たので、Watch OS側の実装を行います。
今回のサンプルでは、Watch OSが起動した際に、iPhone AppへSessionを開始するコードとなっています(この際はloadDataメソッドをコールします)。

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {
    
    let wcSession = WCSession.default
    
    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        
        // WCSessionの開始
        if WCSession.isSupported() {
            wcSession.delegate = self
            wcSession.activate()
        }
        
    }
    
    override func didAppear() {
        super.didAppear()
        
        print("[Info] didAppear")
        if wcSession.isReachable {
            print("[Info] reachable")
            loadData()
        } else {
            print("[Info] not reachable")
        }
    }
    
    override func willActivate() {
        super.willActivate()

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

    func loadData() {
        let message = [ "message" :  "test"]
        wcSession.sendMessage(message, replyHandler: { replyDict in
            print("[INFO] responce action data")
            print(replyDict)

        }, errorHandler: { error in
            print(error)
        })

    }
}

// MARK: - Session Delegate
extension InterfaceController {
    
    func sessionReachabilityDidChange(_ session: WCSession) {
        print("[Info] sessionReachabilityDidChange")
        if wcSession.isReachable {
            print("[Info] reachable")
            loadData()
        } else {
            print("[Info] not reachable")
        }
    }
    
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("[Info] The session has completed activation.")
    }
    
    func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
        
        // iPhoneからのデータを受け取る
        print("[Info] didReceiveMessage")
    }
}

上記コードの、

        wcSession.sendMessage(message, replyHandler: { replyDict in
            print("[INFO] responce action data")
            print(replyDict)

        }, errorHandler: { error in
            print(error)
        })

この部分で、iPhone App側に通信を送っています。
ここで、例えばWatchManagerのメソッドを以下のように変更します。

    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        print(message)
        var result: [String: Any] = ["success" : false]
        replyHandler(result)
        
    }

これで実行すると、 以下の情報が出力されます。

[ "message" :  "test"]

ここまでで、iPhone AppとWatch OS Appとの接続の部分が出来ました

Watch OSとiPhoneとでCoreData等のデータのやり取りを行います。

ここに来て、やっと表題のお話が出来ます…。

これまでは、Watch OS AppとiPhone Appとの通信部分を造ってきたので、
この流れの上で、リソースデータのやり取りを行います。

といっても、設計はごく単純で、
すべてのデータのやり取りはiPhone側で行い、Watch OS側は適宜データの送受信のEventを送るだけです。

最初の方に載せた図で言うと、以下のようにApple Watchから来たEventのレシーバからCoreDataへアクセスし、データが必要であれば、返却する仕組みです。

f:id:project-unknown:20180902005106p:plain

前提として

今回のデータの送受信を行うにあたり、以下のデータ構造をDictionary形式でやり取りします。

Apple Watchからデータを取得するリクエストを投げる時

[
   "kind" : 1,
]

kind : 1はデータ取得を行いたい識別子です。

Apple Watchからデータを更新した際にリクエストを投げる時

[
    "kind" : 2,
    "data" : Any
]

この場合は、kind : 2として、一緒に更新したいデータを送信します。

では、コードの方を見てみます。

iPhone App側の実装

これまで書いてきた、WatchManagerのメソッドを以下のように変更します。

    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        
        var result: [String: Any] = ["success" : false]
        
        guard let kind = message["kind"] as? Int else {
            result["success"] = false
            
            replyHandler(result)
            return
        }
        
        switch kind {
        case 1:
            result["data"] = /* CoreDataからとってきたデータ */
            result["success"] = true

            return
        case 2:
            /* CoreDataのデータ更新 */
            result["success"] = true
            
            return

        default:
            replyHandler(result)
            return
        }
    }

kindで、処理の識別を行い、適宜CoreDataの操作を行います。
最後に結果をreplyHandlerに渡して上げるだけです。

Watch OS側の実装

iPhone Appでデータを受信して、適切なレスポンスを返せるようになったので、
今度はWatch OS側からEventを送信する方法です。

まずは、データを受け取る所ですが、
loadDataメソッドを以下のように改変します。

    func loadData() {
        let message = [ "kind" : 1]
        wcSession.sendMessage(message, replyHandler: { replyDict in
            print("[INFO] responce action data")
            print(replyDict)
            /* replyDictにCoreDataから取得した情報が入っているので、以後適切な処理を行う */
        }, errorHandler: { error in
            print(error)
        })

    }

データ受信の際は、kind : 1でSession通信を行えば、iPhone側でCoreDataにアクセスし、データを返却してくれます。

次に、データ更新は、上記loadDataと殆ど同じですが、念の為載せておきます。
データ更新ように、saveDataメソッドを追加しています。

    func saveData() {
        var message: [String: Any] = [:]
        message["kind"] = 2
        message["data"] = data
        
        wcSession.sendMessage(message, replyHandler: { replyDict in
            print(replyDict)
        }, errorHandler: { error in
            print(error)
        })
    }

これで、適切なタイミングでsaveDataを呼び出せば、保存したいデータをCoreDataに保存することが出来ます。

詰まった所

終わりに、私が盛大に詰まった所として…。
当たり前のように、iPhone AppのWatch ManagerでreplyHandlerを使っていますが、
Apple WatchでSession処理を行う際に、適切なDelegateをiPhone App側で実装していないと通信エラーとなります。

この詳細は、以下にまとめておきましたので、もし詰まってしまった場合の参考にしてください。

www.project-unknown.jp

最後に

Watch OSは他のWidget等と違って、これまでのWidget等と違って、考え方を大きくシフトしないとなかなか造りにくいです。
しかし、WebAPIを用いた実装を行っているんだと、考えると一気に考え方が楽になります。(Appleはここまで考えているんでしょうね・・・さすがや)

最後に宣伝となりますが、
この記事は、現在TIME HACKERでApple Watch対応を行っており、その際の備忘録として記したものです。

もう少しでリリースが見えている所ですので、是非とも皆さんお使いになってくださいね!

img

Watch OS 2 - WCErrorCodeDeliveryFailed が出てiPhoneとWatchでデータのやり取りが出来ない時の対応

はじめに

Watch OS 2からは、Apple WatchがiPhoneアプリから独立して動くことになったので、これまでとは違った書き方が必要です。

この対応を行っている最中に、以下の様なエラーが出て詰まってしまったので、その備忘録を示します。

エラーの内容 WCErrorCodeDeliveryFailed

Watch Extension[3994:434267] [WC] -[WCSession _onqueue_notifyOfMessageError:messageID:withErrorHandler:] CFEDC3E4-FE0F-439D-B884-1CB2EC349DED errorHandler: YES with WCErrorCodeDeliveryFailed
Error Domain=WCErrorDomain Code=7014 "Payload could not be delivered." UserInfo={NSLocalizedDescription=Payload could not be delivered.}

実際に書いていたコードの中身

iPhone側、AppleDelegate

let wcSession = WCSession.default

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    initWCSession()
}
func initWCSession() {
    if WCSession.isSupported() {
        wcSession.delegate = self
        wcSession.activate()
        print("session activate")
    } else {
        print("session error")
    }
}

Watch Extension側

let wcSession = WCSession.default

override func awake(withContext context: Any?) {
    super.awake(withContext: context)
        
    // WCSessionの開始
    if WCSession.isSupported() {
        wcSession.delegate = self
        wcSession.activate()
            
        sendMessageToApp()
    }
        
}

func sendMessageToApp(){

    guard wcSession.isReachable else {
        return
    }

    print("sendMessageToParent()")
        
    let message = [ "toParent" : "OK" ]
        
    wcSession.sendMessage(message, replyHandler: { replyDict in
        print(replyDict)
    }, errorHandler: { error in
        print(error)
    })
}

今回上述しているエラーは、このprintで表示したエラーの中身です。

対応方法

今回の原因は、メッセージを受信する(iPhoneのAppDelegate側)方のレシーバーの処理が足りていませんでした。

AppDelegateに以下のメソッドを追加する事で、レシーバーがメッセージを受信できるようになるので、正常にデータを送信することができるようになります。

func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    replyHandler(["reply" : "OK"])
}

Firebaseのエラーへの対処 - Terminating app due to uncaught exception 'com.firebase.core', reason: 'Default app has already been configured.

はじめに

iOSのFirebaseでは、以下を二重に呼ぶ事は禁止されています(クラッシュします)

FirebaseApp.configure()

この時のクラッシュの内容は以下のようになります。

Terminating app due to uncaught exception 'com.firebase.core', reason: 'Default app has already been configured.

しかし、TodayExtension等、回避しづらいケースがあり、その解決法です。

FirebaseApp.app()を使う

要はFirebaseApp.configure()は、FirebaseAppのオブジェクトを生成し、設定を行う事ですので、
インスタンスが生成されていないかをチェックすれば回避出来ます。

import Firebase

class TodayViewController: UIViewController, NCWidgetProviding {

    override func viewDidLoad() {

        // 省略

        if FirebaseApp.app() == nil {
            FirebaseApp.configure()
        }

        // 省略
    }


}

TIME HACKER メイキング

TIME HACKERリリースしました!

img

TIME HACKERをめでたくリリースすることができました。
まだの人は是非この機会にお試しください!

今回は、TIME HACKERを作るにあたり、その制作過程について記載していこうと思います。
今回の記事では、

  • どんなUX思想で作ったのか?
  • 設計思想はどうしたのか?
  • アプリ実装面での設計は?
  • マネタイズについて

上記を、書ける範囲で載せましたので、 これからアプリを作ろうとしている人へは、制作過程の参考に、
同業者の方へは、苦難の共感が得られれば幸いです。

TIME HACKERを作ることとなった動機

時間が欲しい

世の中のサラリーマンの殆どがそうだと思いますが、とにかく時間が無い。
平日に、スキルアップのための勉強や、アプリ造りを行いたくても、家に着くのが日付をまたがる時間帯になる為、中々実施することができません。

休日にやろうと思うのですが、、いざ休日になったら平日の疲れを癒やすために、ガッツリ休んでしまう等、自分のやりたいことがやれない毎日でした。

もう社会人生活も10年を超えてきて、一向に改善しない毎日に諦めの気持ちすら抱いていた際に、一冊の本と出会いました。

ライフハック大全

ふと手にとった、以下の「ライフハック大全―人生と仕事を変える小さな習慣250」という本が私の中で革命的でした。

何個もあるライフハックは、殆どが私の血肉になる位に、良い知識を仕入れる事ができたのですが、その中でも、最も私の中でためになったのは、

自分の1日の行動ログを取って、無駄に過ごしているところは無いか?を見返すことで時間を捻出する術

これにものすごく感銘を受け、その時のツイートが以下。

実際にこのライフハックを実施してみました。
私は1日の中で、以下の時間をもっと取りたいと考えています。

  • 読書をして自己啓発と勉強
  • PCを使ってプログラムや新技術の勉強
  • 個人アプリ、ゲームの開発

この観点を元に、1日の行動ログを記録し、時間を生み出すことが出来ないか分析してみます。
(メモ帳に走り書きしていた為、時間が入っていたり入っていなかったりしてます。)

  1. 8:00 起床
  2. 出る準備 20分 8:20
  3. 家を出てからバスにならぶ 10分 8:31
  4. バス乗車時間 13分 8:45
  5. 電車待ち時間 16分 9:02
  6. 特急 30分 乗り換え後、○○から△△ 20分 計50分 9:50
  7. 以降会社
  8. エレベーター待ちと朝の準備 16分 10:06
  9. タバコ 5分
  10. インシデント対応 41分 10:53
  11. 今日のタスクプランニング 9分 11:00
  12. ○○ミーティング 24分 11:27
  13. トイレとタバコ 15分 11:45
  14. 今年度のプロジェクト目標について議論 15分 12:00
  15. 昼飯と移動 30分 12:30
  16. タバコ 15分 12:45
  17. 目標についての議論 15分 13:00
  18. 溜まっていたSlackやり取り 21分
  19. タバコ 15分 13:40
  20. 雑務 15分
  21. 新規企画のKPI試算 vol1 45分 14:40
  22. タバコ 10分
  23. 新規企画のKPI試算 vol2 48分 15:45
  24. タバコ10分
  25. 部下面談 36分
  26. タバコ 10分
  27. ○○チーム向けプレゼン資料作成 10分
  28. △△MTG 40分
  29. ☓☓MTG 1時間30分
  30. タバコと帰る準備 10分 19:30
  31. ○○まで移動 20分
  32. △△まで 35分
  33. ジムで運動 1時間
  34. ジムの風呂 30分
  35. 買い物 10分
  36. バス停まで移動 5分
  37. バス待ちとバス乗車 18分
  38. タバコ 5分
  39. 家移動2分
  40. 家に着いて落ち着くまで 7分
  41. ダラダラ過ごす 2時間
  42. 風呂とダラダラ 30分
  43. 就寝 1:17

この日を振り返って見ると、例えば、以下を改善できそうです。

タバコ時間を読書時間に企てる

タバコの時間をタバコ + スキルアップのための読書(Kindle)を行うとしたら、80分読書時間が確保出来ます。
全てのタバコの時間を企てるのは難しいのと、読書の開始時には思い返しの時間も必要な事を鑑みても、50分は読書時間が確保できます。

移動時間を読書・勉強時間に企てる

また、バスの時間がトータル31分。電車の時間は特急が65分、在来線が40分。
バスと在来線は基本立つので、ここを読書に当てると71分。

特急は座ることが出来ますが、片道30分程度なので開発するには時間が足りなく見えるので、PCを使った勉強時間に企てると、65分勉強時間が確保できます。

ここまで考えると、家に帰るまでに、以下の時間を確保することがわかります。

  • 読書時間を121分
  • 勉強に65分

ただ、この勉強・読書時間は断片化した時間なので、効率は悪いですが、それでも家に変えるまでに、これだけの事が行えるので、

家では、アプリ開発に集中することができる。いやアプリ開発だけしていていいのです。

これまでだと、「勉強もしないと」「本も読まないと…」と考えがよぎって中々集中出来なかったのですが、
一日の無駄な時間、スキマ時間に十分に勉強を行えているので、家でやる必要が無いのです。

そして、家でダラダラとする時間を2時間から半分削るだけで、1時間確保出来ます。
更にストイックに削れば1時間半〜2時間確保できるでしょう。

ここまで見てわかるように、

1日の時間を細かく記録する事と、分析する事は、忙しいビジネスマンに取って、ものすごく有益である

ことがわかります。

そして、有益で有ることがわかった次は、もっと楽に記録したい・分析したいと思い、今回の「TIME HACKER」アプリに繋がります。

私自身の生活の質を向上させるために、どうしてもこの手のアプリが欲しかったのと、私の中でしっくり来るアプリがStoreに並んでいなかったので、何としてでも手に入れたかった。

気がつけば、全ての開発や勉強を停止して、着手していました。

(なんだかんだで7ヶ月も造ってた…)


仕様検討

コアバリューを決める

いつもは凄い悩む所だったのですが、ここは作り出した動機が動機なので、すぐに固まっていました。

アプリ制作初期は

無駄な時間を管理し、自分のスキルアップに企てたい時間を確保する

で、最後までここは変わらなかったのですが、
もっと範囲を広げて、以下を最終的なコアバリューとしました。

自分の時間を管理し、生活改善のサポートをするアプリ

ターゲット層を考える

ターゲット層は、女性でも・男性でもありませんし、
特段ペルソナも考えていません。

TIME HACKERでは、以下のユーザーをターゲットとして捉えています。

  • 気づいたら時間ばかり過ぎている人
  • 忙しくて自分の時間が確保出来ない人
  • 自分のスキルアップの時間を確保したい人
  • 日々の生活をもっと有意義に送りたい人

UXを検討する

大体のアプリに言えることですが、
このアプリはUXが特に肝となります。

  • 何より、毎日、しかも小刻みにアプリを立ち上げて行動のログを計測しないといけない。
  • それには、ユーザーのアプリ上の動作だけではなく、生活のリズムを一瞬でも妨げてはいけない。
  • 特に、行動ログを記録忘れた場合は、一気に記録するモチベーションが下がります。なので記録忘れを防がなければならない。
    • (結局、ここのサポート部分は1stリリースからは見送りました。)

上記をいろいろ考えてUXを造っている際に、取っていたメモが以下です。

f:id:project-unknown:20180528223801p:plain

f:id:project-unknown:20180528223811p:plain

結局、当時考えていた機能の半分くらいしか実現できていないのですが…。

UIを検討する

ここもUX並にメチャクチャ苦労しました。
なんだかんだで、2ヶ月近く、あーでもないこーでもないをやっていたんじゃないかしら。

ちなみに、最初期の頃に考えていたUI

f:id:project-unknown:20180713010102p:plain:w400

全く違いますね…。
今見ると、個人的に結構しんどいUIですが、当時はこれでいける!!と信じて止みませんでした。
結局、2日位、寝かせてから再度この画像を見て、くっそだせぇって思って、やり直しました
その時の温度感だけで決めるのではなく、一度寝かせるのは大事ですね。

次に考えたのが、以下のUIです。
文字情報を見るのではなくて、行動をアイコン化し、なれたら無意識で記録ができる事を狙っています。
また、無意識で押せる事を狙って、アイコンも結構大きめに設定してます。

f:id:project-unknown:20180713010403p:plain:w400

だいぶ、今の原型が見え隠れしていますね。
ここから、紆余曲折を経て、以下の様な画面で落ち着きました。

f:id:project-unknown:20180713010500p:plain:w400

マネタイズを検討する

TIME HACKERで、現在実装済みのマネタイズをご紹介します。
今後のマネタイズは敢えて載せません。
お金の匂いがプンプンする機能が追加されたら、「あっ、マネタイズに走ってるな」とでも思ってください。

  • アプリトップにバナー広告
  • 一定の回数計測を行った際のインタースティシャル

バナー広告

バナー広告は、KeyHolderでも実装しているのと、KeyHolderでも操作の邪魔にならないところに設置していますが、結構馬鹿にならないくらいの収益を上げてくれます。
UXの所でも述べていますが、TIME HACKERは、UXがとにかく命です。ユーザの行動を阻害する事をしようものなら、記録が面倒になり一気に使わなくなります
なので、バナー広告を設置するにあたり、アプリの操作を阻害する事はしないような画面設計にしています。

インタースティシャル広告

KeyHolderでは、インタースティシャル広告を出さずに本当に公開しました。
というか、KeyHolderはシンプルを極めすぎてインタースティシャル広告を入れる余地が無く、入れようものならアプリとしての価値を大きく欠損しかねるので、断念しました。

なので、TIME HACKERではインタースティシャルは仕様検討の総初期から、なんとかして入れられないかを深く検討しています。
(書けば書くほど、金儲けが全面に出てしまうので、引かれるリスクはありますが、それでもこの記事を記載しています)

バナー広告の項でも記載しましたが、TIME HACKERは何よりUXが命なので、インタースティシャル広告を入れる箇所には最新の注意を払っています。

TIME HACKERでインタースティシャスは、一定の時間、一定の回数、行動ログを取った際に、行動ログを記録したタイミングで広告表示を行っています。
これは、ユーザーとして考えた際に、行動ログを計測する直前に広告が表示されたら、ユーザーの行動を阻害するリスクが非常に高まる為、記録を開始したタイミングで広告を表示することにしました。

行動ログを記録した後に広告を表示しているので、マネタイズとしての期待値は大きく下がると思いますが、私が考えている一番のリスクは、

眼の前の収益に目がくらんで、ユーザの行動を阻害する事で、アプリをアンインストールされる事を一番のリスクとして捉えています

また、上記で一定の回数と記載しましたが、初回リリースまでにどの回数が妥当なのか?の結論を出すことが出来ませんでした。

ですので、FirebaseのA/Bテストを採用して、インタースティシャル広告を出すタイミングの検証を行っています。

www.project-unknown.jp

ここまでは、アプリ設計のお話でした。
次に、実装面でのお話をしていこうと思います。

実装開始

さぁ、ここからお祭りの始まりです。
いやぁ…とにかく紆余曲折しまくった…。

プロトタイプ開発

今回のアプリ開発では、特に真新しい事を中心に、やりたいことは何でもやろうと思い色々tryしました。

大きい所で言うと、

Carthageにも挑戦しましたし

www.project-unknown.jp

Quickにも挑戦しましたし

www.project-unknown.jp

RealmSwiftにも挑戦しました

www.project-unknown.jp

Firebaseにメインの解析機能を移管しましたし、
Firebaseを使ってA/Bテストにも挑戦しています。

www.project-unknown.jp

以下の「Code Complete」等の技術書も読み漁り、開発設計についての色々な挑戦も行いました。

そして気づいたのが、

tryばっかしていて、いつまで経っても完成しない

特に、開発設計のところは、色々な本を読みすぎて、読む本全てに影響を受けて設計が日々変わりすぎていっているのが色々と足を引っ張りました。
こんなところにもエターナル現象は顔をだすんですね。

なので、プロトタイプとしてある程度のものが出来上がったタイミングで、tryは封印し、アプリ完成にフォーカスする事にしました。

以後は、アプリ制作時に大きな節目について記載します。

DBシステムの変更

これまでDBをRealmSwiftで開発していたのですが、
諸々の事情で、結局CoreDataに移しました。

RealmSwiftはCoreDataと違い、Objectを生成して値を突っ込むだけで物理領域にも保存され、直感的にデータを扱うことができる優れもので、開発当初はRealmSwiftを崇拝してやまなかったです。

しかし、

  • DeleteRuleが使えない
  • マイグレーション時に、意図しないデータが潜り込む
  • トランザクション範囲が操作し辛い

等、不満が次々と湧いてきて、最終的にCoreDataにデータを移管しました。

ココらへんは、ここに詳細を記載しています

ただ、RealmSwiftとCoreDataとでは、やはりアプリ設計が異なってくる為、ここを直すために、結構作り直しを余儀なくされました。

(それでもDAOを全て直す気力は無く、一部のDAOはRealmSwift前提とした設計のままになってしまっています)

アイコンをやっぱり全部自分らで造ることにする

開発完了が大きく遅れたのは、ここが原因です。ですがこれは英断だと思っています。

これまで、アイコンをフリーで提供してくださっているサイト様から使わせてもらうつもりでおり、完成直前までこれで行くつもりまんまんでした。

ただ、せっかく造るんだからアイコンも自作する事に決め、ぽぽたが泣きながら作業開始しました。

元々100を超えるアイコンを他サイト様のフリー素材から用意していたのですが、
流石に、この量を開発過渡期に用意するのは無理があったので、数を70ほどにしぼりました。

https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716161615.png
(作成したアイコン一覧)

この辺の詳細については、ぽぽたが詳細の記事に起こしています。

www.project-unknown.jp

始まるエターナル化

今回一番厄介だったエターナル化は、機能のエターナル化より、コードのエターナル化でした。
一つの機能を造る度に、4,5回リファクタリングを行ったり。
一度仕上げたコードを、設計しなおしたり。
自分が満足するまで何度も何度も作り直してました。

ただ、これって、

システムの保守性だけ上がって、いつまで経ってもリリースできなくなる自体を引き起こしていました

確かに保守性は非常に大事なのですが、とにかく時間がかかる。
プライベート開発なので納期を気にしないというのが更に拍車をかけてしまっていました。

塩梅を調整するのが本当に難しい…。
このリファクタリングも最終的には自分の中で最低限に抑えるようにしました。

例えば、以下の時だけリファクタリングを行うようにしています。

  • DRYを守れていない
  • SOLIDの原則に準拠していない
  • ViewからModelクラスの細かいメソッドにまでアクセスしようとした時

観点は以下です。


DRYを守れていない

似た機能が乱立した場合、開発過渡期になればなるほど、修正漏れによるさらなる不具合、進捗の遅れに響きます。
ここは徹底的に潰しました。
ちなみにDRYが有名になったのは、以下の「達人プログラマー」からですね。この概念は道を踏み外しそうになった際の羅針盤になり得る考えでいつも大いに助けられています。


SOLIDの原則に準拠していない

iOSライクなMVVMとかではなく、またはプロトコル指向でもなく、オブジェクト指向の1つの考えであるSOLIDをとにかく大事にしました。

プロトコル指向で統一したかった所はありますが、Swiftのプロトコル指向は熟成されていません。これを守るがあまりに無駄で汚いコードになりがちです(会社の現場でこれに囚われて、意味不明なコードを書くエンジニアを散々見ています)。

また、何でもかんでもMVVMを採用するのは愚の骨頂です。ただクラスを増やすだけで、自己満足だけ満たされるコードになるでしょう。
こういうのは適材適所であるべきです。

なので、SOLIDの原則だけを準拠することにしています。
これはDRYを包括している思想ですので、DRYルールと非常に相性が良い。

以下は、SOLIDの原則について記載しています。

  • S - 単一責任の原則 (Single Responsibility Principle)
  • O - 開放・閉鎖原則 (Open/closed principle)
  • L - リスコフ置換原則 (Liskov substitution principle)
  • I - インタフェース分離の原則 (Interface segregation principle)
  • D - 依存性逆転の原則 (Dependency inversion principle)

当たり前だけど、気がついたら守れていない原則ですが、ちゃんと守れている時の効果は絶大です。
このルールには非常に気を使いました。


ViewからModelクラスの細かいメソッドにまでアクセスしようとした時

これはSOLIDの一種ですね。
ViewからModelのデータをごにょごにょするのはイケていません。
その場はそれで良いかもしれませんが、後ほど似たようなデータアクセスが必要になる度にインタフェースを作る羽目になります。


終わらないローカライズ

TIME HACKERは日本以外の世界で発信しています。
細かく言うと、GDPRを受け、EU諸国を除いた全世界に発信しています。

アプリ内のローカライズは、元々ローカライズを意識した実装を行う癖があったので、さほど問題無かったのですが、
問題は、ブログに紹介やらヘルプやらをやたらと親切に記載してしまっていた為、これら全てをローカライズする必要ができました

いやぁー、これはアプリをリリースするのを辞めるか悩むくらい霹靂しました。
最後の1ヶ月間は、このローカライズを延々とやっていた気がします。
実際にアプリ以外のローカライズは以下を対応しました。

繰り返しますが、、

アプリのローカライズ以上に、このローカライズの方が遥かにしんどかったです

しかも、後述しますが、最終的にこの1ヶ月の作業は殆ど無駄になり、申請日にプロジェクトメンバー総掛かりで作り直してます。

最後の追い込み

具体的な日付だと、2018/7/15(土)にプロジェクトメンバーで集まり、
最終テスト、ヘルプページやプロモーションページの仕上げを行って、
夜までに申請を上げて打ち上げを想定していたのですが…、

相次ぐ不具合
まさかの、申請日にUI変更
UI変更に引っ張られて、全ての画像変更

が発生し、気づいたら申請したのは、2018/7/16(日)の夜でした…。
特に、UI変更が本当にしんどかった…。
これにより、具体的には、

以下の、ストアキャプチャ作り直し

英語版iPhoneX

f:id:project-unknown:20180716013728p:plain

英語版5.5 inch

f:id:project-unknown:20180716013744p:plain

日本語版iPhoneX

f:id:project-unknown:20180716013811p:plain

日本語版5.5inch

f:id:project-unknown:20180716013830p:plain

というか、最後の2日間、殆ど画像の差し替えしかやってない気がする…。


くらうリジェクト

今書いているブログ記事もそうですが、
申請を待っている間に、私含め、ぽぽたもTIME HACKER関連の記事を色々書き溜め、申請が通ったらアプリのリリースも含め、ブログ記事のリリースをするために待ち構えていましたが、ものの見事にリジェクトをくらいました

内容は

2. 4 Performance: Hardware Compatibility
Guideline 2.4.1 – Performance – Hardware Compatibility

iPadでの表示が崩れているから、ちゃんとUI整えろよというご指摘です。

実は、リリース前にiPadで表示が崩れる事を知っていましたし、Appleの審査する人はiPadで審査するということも知っていました。

(iPhoneアプリであっても、iPadに内蔵されているiPhoneシミュレータで動くべきであるという考えなので、私が知る限りでは、iPadで審査を行っていそうです。)

ですが、まぁ、操作できない事ないじゃんという謎の過信で、対応を見送っていたところを、案の定指摘されました。

もう、目も当てられない…。

こちら、OSSを使ってUI表現していた所だったので、ライセンスの問題もあり、手を加えるわけにもいきませんから、その場しのぎとして、表示崩れが発生する端末では、一部機能を削除して再申請しました。

初回リリースで諦めた事

たくさんあります。
本当にたくさんあります。

何故か申請日にiTunes Connectの調子が悪くて、iTunes Connectにバイナリアップロードするのに数時間かかった為、
心が盛大にポッキリ折れて、気づいていた簡単なバグはそのままにしてしまっていたり、

UXの検討の時に記録忘れ防止のための通知機能も、
通知タイミングを間違えたらユーザーの不満がものすごく上がりそうという、チキンハートがひょっこり顔を出して見送ったり、

本当に色々積み残しています。

ざっくりと列挙すると、以下の機能を初回リリースでは諦めています。

通知機能

上述した通りです

Apple Watch

Twitterに思いっきりApple Watch化すると記載しているのに、工数の兼ね合いという大人の都合で諦めてます。

Widget対応

大人の都合

分析機能の何点か

最終的にTIME HACKERの押しにしていきたいのですが、
まずは、ユーザーの行動ログを貯めることが何より優先すべき事だったので、ここの機能も結構諦めています。

アプリ内課金

やりたかった…。
本当にやりたかった…。
できなかった…。

KPT

締めくくりとして、今回のTIME HACKERのKPTを考えたいと思います。

Keep

自分の信念を最後まで貫いた

ライフハック大全を読んで、是非このアプリを使いたい、そして造りたいと言う思いを最後まで貫けたのは良かった。

このアプリは、万人受けするアプリではないという事はわかっています。
しかし、ターゲット層項目に記載した人に長く使ってもらう事だけを想定して造っています。

エターナル化を防いだ

本当はAppleWatch、Widget、分析機能の強化、Push等もっとやりたかったけど、これを全て捨てました。
初回リリースでは、ユーザーのログを貯めることを何より重要視し、それ以外の機能は二の次にしました
結果として、この判断を行わなければ、リリースは来年になっていたでしょう。

Problem

何でもかんでもやりすぎた

最低限の事だけしかやっていないつもりだったのですが、今考えると色々手を広げすぎたと思います。

特にローカライズはリリース後でも良かったかもしれません…が、iTunes Connectの仕組み上、プライマリー言語を最初から英語に指定していないと、後々更に面倒な事になっていたので、これでよかったのかもしれませんが…。

またヘルプやプロモーションはやりすぎました、これこそリリース後でも良かった。

至る所でチキンハートが顔を出した

RealmSwiftをやめたのも結局の所、チキンハートが問題でしたし、
チキンハートが顔を出したせいで、ハレーションを生みそうな機能を見送ったり、もっと強気に造っても良かった。

プロジェクトとして、集まって作業する頻度が少なすぎた

最近はリモートワークが流行っていますが、やはり対面でやり取りする作業だと、それぞれのクオリティ・速度が全然違います。

特に申請日に集まって、最後のテストを行った際の不具合の出方がやばかったです。
もうちょっと早めに集まってテストするなどすれば、問題の早期発見に気づけたでしょう。

明らかにリスクがあるとわかっていた不具合を放置した

最後のリジェクトの所ですね。
これは本当にもう愚かだったとしか言いようが無い。
面倒くさがって、対応を先送りにした結果としか言いようが無い。

Try

プロジェクトとして集まる頻度を増やす

これはProblemの裏返しですね、クオリティ・速度を上げる意味で、無理にでも集まって集中して造る時間を確保したほうが良いです。

チキンハートをどうにかする

なにかする度に、いや契約が…とかいや請求一気にきたら怖いしー…とか、
出す前にでもでもだってを発揮して、チャンスをことごとく逃しているのがとにかくもったいない。
やる前に後悔するなら、やって後悔するの精神を持とうと思います。

多少面倒でも正しいことをする

これは最後のProblemの不具合と気づいていて目を背けたものに掛かります。
結局バグの放置や不具合の放置は、問題の先送りでしか無いですし、別な言い方をすれば、ローンと同じで先送りにすれば金利(より面倒な工数)が増えるものとし、リスクがあるものに関しては、速攻潰していくマインドは持ち続けなければと痛感しました。

最後に

自分たちで言うのも変な話ですが、TIME HACKERは、自分の時間を生み出す為に有用なツールに仕上がっています。
また、ユーザーの声を聞いて、機能追加やアイコン追加も行っていこうと思いますので、気になる事などありましたら、いつでも、以下のフォームよりお問い合わせください!

TIME HACKER ご要望・お問い合わせ

是非、TIME HACKERを使って、毎日の生活を少しでも改善していきましょう!

RealmSwift vs CoreData

はじめに

iOSのローカルデータベースとして、これまでは、CoreDataが主流でした。
CoreDataの他だと直接SQLiteを弄ったり、ラッパーとなるFMDBなどのOSSがありましたが、Appleが提供している機能として、やはりCoreDataのシェア率は高かったと思います。

実際に、以下はAppleが公開している資料ですが、
CoreDataを採用する事で、純粋にSQLを叩くよりスピードが上がるや、メモリ効率も良い等、夢のようなツールとして紹介されています。

f:id:project-unknown:20180717010316p:plain:w430

しかし、CoreDataは学習コストが非常に高く、扱えるようになるまで時間が掛かったり、 扱えるようになったとしても、他のDBMSライクに扱おうとしたらどハマりしたりと、非常にクセが強いです。

iOSアプリの現場で、CoreDataを採用しているPJ等に配属された際に、困った人も多いのではないでしょうか。

そんな中登場したのが、RealmSwiftです。

f:id:project-unknown:20180717010114p:plain:w430

Realmは、公式サイトから紹介を引用すると

Realm Swiftはアプリケーションのモデル層を効率的に安全で迅速な方法で記述することができます。

実際に使ってみた感想だと、とにかく実装が楽です。
あまり考えなくてもデータの永続化もでき、UserDefaultsみたいなお手軽感があります。

しかし、

結論から先に言うと、TIME HACKERアプリを開発中に、私はCoreDataを選択し、RealmSwiftベースで作ってきたアプリの構造をCoreDataに移しました。

他のサイトでも、RealmSwiftとCoreDataの比較がなされているところが多いですが、私の主観でも書いてみようと思います。

リレーションシップをやろうとすると面倒

RealmSwiftでリレーションシップライクな事をやろうとすると、以下のように記述します。

FirstEntity

import UIKit
import RealmSwift

class FirstEntity: Object {
    @objc dynamic var id = NSUUID().uuidString
    @objc dynamic var second: SecondEntity? = nil                

    override static func primaryKey() -> String? {
        return "id"
    }
}

SecondEntity

import UIKit
import RealmSwift

class ActionEntity: Object {
    @objc dynamic var id = NSUUID().uuidString
    
    let firstEntity = LinkingObjects(fromType: FirstEntity.self, property: "second")  // FirstEntityがぶら下がる
}

上記のように書くと、SecondEntityにFirstEntityがぶら下がる形となり、
SecondEntityにFirstEntityを登録する事で、データを一気に登録・参照する事が可能になります。

これだけなら、非常に便利なのですが、 削除する時(Delete Rule)が問題になります。
RealmSwiftでは、DeleteRuleを設定することができないため、他のDBMSにあるように、親となるデータを削除した際に、子のデータも一緒に削除することはできません。
(私が開発していた時に、気づいていなかっただけなら申し訳ありません。)
ですので、RealmSwiftでデータ削除する際に、親も子も手動で削除していく必要があります。

反面、CoreDataの場合だと、

f:id:project-unknown:20180716180641p:plain:w430

こうする事で、親のデータを消したら、子のデータまで一気に消えます。


トランザクションが分かりにくい

これは完全に慣れの問題ですが、RealmSwiftの場合、コードブロック内でトランザクションが貼られます。

逆にいうとトランザクションから外れてコードを書くと思わぬところでデータ更新が走って詰まった事がありました。

CoreDataのコンテキスト内に更新ロジックを書いて、最後にsaveする書き方に慣れてしまったので、ここは凄く違和感を感じました。


マイグレーションに違和感がある

例えば以下の構成があったとします。

import UIKit
import RealmSwift

class FirstEntity: Object {
    @objc dynamic var id = NSUUID().uuidString
    @objc dynamic var updateDate? = Date()
}

これにデータを突っ込んで、
アップデートのタイミングで、以下のようにカラム名を修正します。

import UIKit
import RealmSwift

class FirstEntity: Object {
    @objc dynamic var id = NSUUID().uuidString
    @objc dynamic var createdDate? = Date()
}

この時、これまで入っていたDateが初期化されて、1970年が入ってました。

単純にマイグレーションする際の考慮漏れはありますが、CoreDataの場合だとここでクラッシュして気付けます。

RealmSwiftの場合だとデータを登録して結果を見て初めて気付くと言うところが個人的にはしんどいです。

実際に個人で開発しているときは、コアな箇所だとユニットテストを書いたり、ある程度のテストをおこないますが、それでも余暇の時間を使うのもあってテスト漏れは発生しやすいです。

この時に気付けるのか?というのがCoreDataを採用する1番の動機となりました。


締めくくり

結局のところで言うと、RealmSwiftに慣れていなくて、CoreDataは散々使ってきたので慣れていた。
が、CoreDataを採用した1番の理由です。

RealmSwiftは触ったから分かる素晴らしいものですが、私の学習が追いつかなかった。
RealmSwiftはサーバとのデータのやり取りも非常に楽に実装できるよう、サポートされていますし、いずれは使いこなせるにしていきたい限りです。

最後に

TIME HACKER

記事の途中で記載しましたが、今回の記事は、TIME HACKERを開発中の記事となります。

是非お使いください!

UIViewからはみ出したViewを見れなくする方法

はじめに

UIViewは、初期設定だと画面からViewがはみ出してもそのまま表示されてしまいます。

例えば以下のように高さ230pxのUITableViewCellがあるとして、

f:id:project-unknown:20180516230227p:plain:w400

UITableViewで表示する際に、最初隠したい為に、44pxとした場合、以下のように崩れてしまいます。

f:id:project-unknown:20180516230328p:plain:w400

今回はその解決法です。

解決方法 clipsToBounds を使う

今回の事例では、clipsToBoundsを使うと簡単に解決出来ます。
clipsToBoundsと言えば、UIImageViewでimageがViewの外に出ないようにする時によく使われますが、UIViewに対しても適用されます。

今回は、UITableViewCellのUIViewの範囲を出てほしくないので、Cell本体にこの設定を入れます。

class LogDetailDateTableViewCell: UITableViewCell {
    override func awakeFromNib() {
        super.awakeFromNib()
        self.clipsToBounds = true
    }
}

これでUITableViewCellの描画範囲を超えた描画は行われません。

f:id:project-unknown:20180516230908p:plain:w400

必要な時に、今回の例で言うとDatePickerを載せたCellを足すと言うのもありますが、今回の例では、日付を管理するLabelとDatePickerを同一Cellとして扱ったほうがリソース管理の意味では良いので、高さ改変に耐えれる方法のご紹介でした。

UINavigationControllerに複数ボタンを並べる方法

はじめに

アプリによっては、ナビゲーションの部分にボタンを複数設置しているものもあります。

提供中のKeyHolder もトップ画面のナビゲーションに複数配置してます。

f:id:project-unknown:20180515221532p:plain:w300

この記事では、このやり方について紹介します。

Storyboard上、Xib上で配置

UINavigation上に、Bar Button Itemを複数配置するだけで、実現可能です。

f:id:project-unknown:20180515221137p:plain

(昔はこれ出来なかった気がするのですが、いつのまにか出来るようになってました。記憶違いでしたら申し訳ありません。)

後は、他のパーツ同様にOutlet接続すればコード上からも操作できます。

コード上で配置する

コード上で配置するには、以下のコードの様に、配列にNavigationItemを入れて、登録する方法です。

func setupNavigation() {
    let button1: UIBarButtonItem = UIBarButtonItem.init(
        image: <画像>,
        style: UIBarButtonItemStyle.plain,
        target: self,
        action: #selector(button1Proc))
        
    let button2 = UIBarButtonItem.init(
        image: <画像>,
        style: UIBarButtonItemStyle.plain,
        target: self,
        action: #selector(button2Proc))
    categoryButton.tintColor = tintColor
        
    self.navigationItem.rightBarButtonItems = [button1, button2]
    }

最後に

この記事はStoryboardでも出来るようになってるすげー!で感動したのが原因で書いてます。 どちらも一長一短ですが、私的にはコードがスッキリするというのもそうですが、Storyboardが提供しているのならそちらに任せてしまって、コードは必要な時のみに書く方が責任範囲の分割が分かりやすいので、Storyboardを利用していきそうです。

Swift 構造体の使い方 - 基本定義, イニシャライザ, メソッドを扱う

はじめに

構造体とは値型のデータを構造的に持つものです。
特にSwiftの構造体は、その中でメソッドを作成出来たりと、クラスと何が違うねん!と思いますが、

構造体はクラスと違って代入や関数呼び出しの際、データの実態がコピーされ、新しいインスタンスが渡されるのが大きな特徴です。
つまり、構造体のデータの実態は、常にプログラム内のどこか1箇所から参照されているだけとなります。

この特性上、冒頭でかいた通り、構造体はデータという考え方で設計すると考えやすいです。

逆に、複数の箇所から同じ実体を参照したい場合はクラスを用いると良いでしょう。

今回は、構造体の使い方について取り上げます。

構造体を定義する

以下は構造体の定義の例です。

/// プレイヤーデータ
struct Player {
    let name: String  // 名前
    var hp: Int       // ヒットポイント
    var mp: Int       // マジックポイント
    var atk: Int      // 攻撃力
}

上記のように構造体は「struct」を使って表現します。
また、上記例で言う「name」などはプロパティと呼ばれ、変数「var」の場合は変更可能ですし、「let」の場合は一度値がセットされたら変更不可です。
ここは、通常の変数と定数と同じ扱いですね。

構造体をインスタンス化する

上記で記載した構造体を実際にインスタンス化して活用する例です。

/// プレイヤーデータ
struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
}

let hero = Player(name: "勇者", hp: 100, mp: 20, atk: 30)
let priest = Player(name: "僧侶", hp: 60, mp: 50, atk: 10)

print(hero.name) // 「勇者」が出力
print(priest.name) // 「僧侶」が出力

インスタンス化は上述の通り

定数 or 変数 = 構造体名(イニシャライザで指定する引数)

で指定します。
ここで注意が必要なのは、先程構造体のプロパティで定数「let」が指定されていたら、1度セットした値は書き換えられなく、変数「var」が指定されていたら、後で書き換えることが可能とかきましたが、

インスタンス化するときに、定数で構造体を宣言すると、例え構造体のプロパティが変数であっても外部から書き換えはできません。

/// プレイヤーデータ
struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
}

let hero = Player(name: "勇者", hp: 100, mp: 20, atk: 30)
let priest = Player(name: "僧侶", hp: 60, mp: 50, atk: 10)

hero.atk = 100 // ここでエラー

書き換えが発生する場合は、変数でインスタンス化をしておきましょう。

var hero = Player(name: "勇者", hp: 100, mp: 20, atk: 30)
hero.ark = 100 // これは問題なし

構造体の初期化 (イニシャライザ)

インスタンス生成のタイミングで値を全てセットする

構造体をインスタンス化するで記載した例だと、イニシャライザである「init」がありません。

この場合、構造体をインスタンス化する際に構造体のプロパティ名に対して値を渡してセットする事ができます。
例で言うと、

let hero = Player(name: "勇者", hp: 100, mp: 20, atk: 30)

上記の「勇者」「100」「20」「30」の部分がそうです。

構造体に予め値をセットしておく

構造体のプロパティに予め値をセットすることも可能で、以下がその例です。

struct Monk {
    let name: String = "モンク"
    var hp: Int  = 200
    var mp: Int  = 0
    var atk: Int  = 40
}

let monk = Monk()
print(monk.name)  // モンクが出力

データの初期値が定まっている場合は、このように初めから値をセットしておくと便利ですね。
また上述しましたが構造体のプロパティは、変数で定義しているものは後から変更が可能なので、必要に応じて書き換えれます。

monk.hp = 100
print(monk.hp) // 100が出力

構造体のプロパティに予め値をセットしていても、インスタンス化のタイミングでプロパティに値を別にセットすることも可能です。

struct Monk {
    let name: String = "モンク"
    var hp: Int  = 200
    var mp: Int  = 0
    var atk: Int  = 40
}

let monk = Monk(hp: 100, mp: 20, atk: 30)
print(monk.hp)  // 100が出力

上記例では、nameは定数で宣言しているので、変数部分を設定しています。

イニシャライザの定義 (init)

これまでは、予め値をセットするや、インスタンス化のタイミングで全項目の値をセットする形でしたが、
initを使うことで柔軟に初期値設定ができます。

以下の例では、initでキャラクターの設定を行っていますが、hp, mp, atkはランダムな値を加算するようにしています。
(ゲームでよくあるキャラクリエイトのイメージ

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init() {
        name = "勇者"
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
    }
}

このように「init」を用いる事で、初期化のタイミングで色々とやれることが増えます。

また、initで他のメソッドと同じように値を受け付けることも可能です。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name  // selfは不要だが、引数と混合するのでつけてます
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
    }
}

var hero = Player(name: "勇者")

ただ注意が必要なのは、「init」を用いると構造体をインスタンス化するの例に出した様に、

let hero = Player(name: "勇者", hp: 100, mp: 20, atk: 30)

この書き方をしたい場合は、自分でinitを用意しないといけません。
なので、イニシャライザのオーバーロードを行って、全てのプロパティの値を受け付けるようにすれば、これまでの書き方ができます。
以下はイニシャライザのオーバーロードを行った例です。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name  // selfは不要だが、引数と混合するのでつけてます
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }
}

var hero = Player(name: "勇者")
var priest = Player(name: "僧侶", hp: 80, mp: 60, atk: 30)

またイニシャライザの処理の書き方にはルールがあり、すべてのプロパティの初期設定が完了するまで、構造体内のメソッドを使うことはできません。
(クラスと同様のルールですね。)

なので、以下はエラーになります。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name 
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        calAtk() // ここでエラー
        atk = 30 + Int(arc4random_uniform(10))
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }

    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }
}

var hero = Player(name: "勇者")
var priest = Player(name: "僧侶", hp: 80, mp: 60, atk: 30)

上記は、init(name:) でatkの設定が終わっていないのに、calAtk()を呼び出そうとしているのでエラーとなります。
この場合は、以下のように、calAtk()を全ての設定が終わった後の場所に移すだけでエラーは消えます。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name 
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
        calAtk()
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }

    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }
}

var hero = Player(name: "勇者")
var priest = Player(name: "僧侶", hp: 80, mp: 60, atk: 30)

構造体でメソッドを使う

すでにこれまでの例で登場してきていますが、構造体にメソッドを定義することも可能です。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name 
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
        calAtk()
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }

    /// ここがメソッド
    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }
}

上記のcalAtk()が構造体内のメソッドです。
ここで注意が必要なのは、構造体のメソッドでは基本はプロパティの値を変更できません。
なので以下のようにかくとエラーが発生します。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name 
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
        calAtk()
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }

    /// ここがメソッド
    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }

    /// ダメージを受けた時の処理
    func damage(num: Int) {
        self.hp -= num   // ここでエラー
    }
}

上記では、self.hp -= num の所でエラーが発生します。
これは大本の構造体の考え方が、データを格納するものだと言う所からきているのかもしれませんね。
ただし、swiftではメソッドに「mutating」のキーワードを付与することで、構造体のプロパティを変更することができます。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name 
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }

    /// ここがメソッド
    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }

    /// ダメージを受けた時の処理
    mutating func damage(num: Int) {
        self.hp -= num   // ここでエラー
    }
}

構造体でGetter / Setterを使う

構造体でもGetter / Setterを使うことができます。

Getter / Setterを使って、これまでのPlayer構造体に歩数管理の「walk」プロパティを追加します。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    var walk: Int {
        get {
            return self.walk
        }
        set {
            walk += newValue
        }
    }
    
    init(name: String) {
        self.name = name
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
        walk = 0
    }
    
    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }
    
    /// ここがメソッド
    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }
    
    /// ダメージを受けた時の処理
    mutating func damage(num: Int) {
        self.hp -= num   // ここでエラー
    }
}

var hero = Player(name: "勇者")
hero.walk = 10
print(hero.walk) // 10が出力
hero.walk = 10
print(hero.walk) // 20が出力

上記のように

プロパティ名 {
    get {
        // 参照される時の処理
    }
    set {
        // 値がセットされる時の処理
    }
}

と記載する事で、プロパティの値をいじる時に専用の処理を記述することが可能です。
今回の処理はGetter / Setterを記載したかったのでかきましたが、このような計算を行う場合は、後述の通りプロパティオブザーバに書いたほうが良いでしょう。

構造体プロパティのwillSet / didSet (プロパティオブザーバ)

プロパティの値が変更される直前「willSet」、直後「didSet」に処理を呼ぶことができます。
先程のSetterの処理をwillSetに、didSetでは歩いていて、毒状態であればhpを減らすようにします。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    var walk: Int {
        willSet {
            self.walk += newValue
        }
        didSet {
            // 歩いた分だけダメージ
            self.hp -= walk - oldValue
        }
    }
    
    init(name: String) {
        self.name = name
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
        walk = 0
        calAtk()
    }
    
    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
        self.walk = 0
    }
    
    /// ここがメソッド
    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }
    
    /// ダメージを受けた時の処理
    mutating func damage(num: Int) {
        self.hp -= num   // ここでエラー
    }
}


var hero = Player(name: "勇者")
hero.walk = 10
print(hero.walk)

これで歩く時の歩数と、歩いた際に毒のダメージを受ける処理が記載できました。
また上記例の様に、

  • willSetでは
    • 新しくセットされる値はnewValue
    • 既存の値は、walk
  • didSetでは
    • 新しくセットされた値はwalk
    • 既存の値は、oldValue

で表現できます。
ちょっとnewやらoldやらややこしいですが、will / didの言葉の意味で推測できますね。

さいごに

冒頭でも記載しましたが、構造体は一見クラスとほぼ同程度の機能が扱えるので、クラスを使うのか構造体を使うのかの判断がしにくい所があり、
わたし自身混乱してきたので、構造体はデータを取り扱うのであるを主眼にまとめてみました。
これまで記載した例のように、データを主眼に構造体を用いると、物凄く使いやすくプログラム自体がとてもスッキリするので是非マスターしたい所ですね。

C# List<T> の使い方 - 要素の追加・検索・Sort・LINQ

2018/03/27 加筆修正。

はじめに

C#には配列と似た機能で、Listと呼ばれる概念があります。
Listは配列とは違い、動的に要素の追加や削除ができます。
この記事では、Listの基本的な使い方から、Listを用いた検索やソート、LINQまでを紹介します。

記事のコードは、最初のListの説明ではあえてDictionaryを採用しています。
理由としては他のサイトでDictionaryを採用したSampleが少なかった為、Dictionaryを用いたListを取り扱う時の参考にご利用ください。

また、後半のLINQのコードでは、実際にUnityで取り扱う際は、自前のDataオブジェクト等を作成して取り扱う事が圧倒的に多いであろうと思われるので、より実践に近いコードを載せてます。

使い方

まず、usingステートメントに以下を宣言します。

using System.Collections.Generic;

このGeneric(ジェネリック)ですが、List型は、「型パラメータ」を利用します。
List

<T>

がGenericを指しています。
なので、usingステートメントで型パラメータを扱えるようにしておかないといけません。

Listの宣言

Listの生成

初期化する際は、以下の例の様に宣言します。

// int型のListを生成
List<int> listValue = new List<int>();

上記はint型のListですが、\<T>の部分はGenericなので他の型も指定出来ます。
例えば、以下はDictionaryを指定した際のListです。

// Dictionary<string, object>型のListを生成
List<Dictionary<string, object>> result = new List<Dictionary<string, object>>();

Listの初期化

Listは配列と同様、初期化のタイミングで値を入れる事が出来ます。

List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};

要素の追加

末尾に追加

末尾に追加する場合は、「add」を用います。

List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};
// 末尾に追加
result.Add(new Dictionary<string, string>() {{"test-key3", "test-value3"}});
途中に追加

途中に追加する場合は、「Insert」を用います。

List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};
// 2番目の要素に追加
result.Insert(2, new Dictionary<string, string>() {{"test-key3", "test-value3"}});

要素の削除

全て削除
List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};
// 全て削除
result.Clear();
指定したインデックスで削除

配列と同様に、指定したインデックスでの削除も出来ます

List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};
// 1番目の要素を削除
result.RemoveAt(1);

要素の取り出し方

配列と同様な取り出し方
List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};
// 1番目の要素を取り出す
Dictionary<string, string> element = result[1];
foreachと組み合わせる

ListはIEnumerableインタフェースを採用しているので、foreachで取り出すことが出来ます。

List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};

foreach (Dictionary<string, string>element in result) {
   // elementに上から順にresultの要素が入る
}

Linqと組み合わせる

LinqはCollectionを柔軟に扱える機能だけあって、Listとの相性が良いです。
以下はLinqの使い方と活用事例です。
冒頭でも記載しましたが、ここからのコードは、以下のDataクラスを利用します。
※これまでのDictionary型ではありません

public class Data {
    public int ID;
    public string Name;

    public Data(int ID, string Name)  
    {  
        this.ID   = ID;
        this.Name = Name;
    }
}

  

使い方

usingステートメントにLinqを追加します。

using System.Linq;
using System.Collections.Generic;

Listの中を検索して取り出し

Find

FindはCollectionの中から1件だけデータを取得します。
以下の例では、キャラクター毎に一意なIDを振り分けているとして、IDに該当する情報を抜き出すやり方です。

List<Data> result = new List<Data> () {
    new Data(1, "hoge"),
    new Data(2, "fuga")
};
Data chara = result.Find(obj => obj.ID == 2);

Where

Whereは条件に合致するものを取得します。
Findと違うところは、合致するもの全て取得するので、IEnumerableで返却されます。
以下の例では、IDが1となっているキャラクター情報を全て取得します。

List<Data> result = new List<Data> () {
    new Data(1, "hoge1"),
    new Data(1, "hoge2"),
    new Data(2, "fuga")
};
IEnumerable<Data> charas = result.Where(obj => obj.ID == 1);

また、IEnumerableで返却されているので、例えばforeachで結果を取り出します。

foreach(var charaData in charas) {
    Debug.Log(charaData.Name);
}

条件に一致する要素を全て取得する

条件に一致する要素を全て取得したい場合は、TakeWhileを使用します。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
// 20以下の数を全て取得する
IEnumerable<int> numberResults = numbers.TakeWhile(x => x < 20);

上記の場合、32,100,203が取得できます。

条件に一致する要素を複数個取得する

一方、全てではなく、指定した個数を抜き出したい場合は、LINQのWhereとTakeを使って実現します。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
// 20以上の数を2個取得する
IEnumerable<int> numberResults = numbers.Where(x => x > 20).Take(2);

上記の場合、32と100が取得されます。

条件に一致する要素を調べる「Any」

条件に一致する要素があるかどうかはAnyを使います。
Anyは条件に一致した要素が見つかった時点で要素の走査をやめるので、高速に動作します。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
if (numbers.Any(x => x > 100)) {
    // 100以上が存在する
} else {
    // 100以上が存在しない
}

Listの中の存在確認する「Exists」

これまでは、条件に合致した要素の取り出しでしたが、単純にListの中でに要素が含まれているか?を知りたい時は、「Exists」を利用します。

// Listの中でID2のものの存在を確認するサンプル

List<Data> result = new List<Data> () {
    new Data(1, "hoge1"),
    new Data(1, "hoge2"),
    new Data(2, "fuga")
};
if (result.Exists(obj => obj.ID == 2)) {
    Debug.Log("true");
}

Listの中を削除

条件に合致したものを全て削除する「RemoveAll」

条件に合致したものを削除する場合はRemoveAllを使います。
以下の例は、キャラクターIDが1のものを全て削除する例です。

// IDが1のものを探して削除
List<Data> result = new List<Data> () {
    new Data(1, "hoge1"),
    new Data(1, "hoge2"),
    new Data(2, "fuga")
};
result.RemoveAll(obj => obj.ID == 1);

インデックスを指定して削除する「RemoveAt」

削除したい要素がどのインデックスにあるのか分かっている場合は、RemoveAtでインデックスを指定して削除できます。

List<Data> result = new List<Data> () {
    new Data(1, "hoge1"),
    new Data(1, "hoge2"),
    new Data(2, "fuga")
};
result.RemoveAt(1); // hoge2が削除される

範囲を指定して削除する「RemoveRange」

範囲指定して削除したい場合はRemoveRangeを利用します。
RemoveRangeは以下のように2つの引数を要求します。

RemoveRange(インデックス番号, インデックス番号から削除する数);

以下はRemoveRangeのSampleです。

List<Data> result = new List<Data> () {
    new Data(1, "hoge1"),
    new Data(1, "hoge2"),
    new Data(2, "fuga")
};
result.RemoveRange(0, 2); // hoge1とhoge2が削除される

Listの数値系の操作

Listの中の最小値・最大値

最小値・最大値を取得する「Min」「Max」

最小値はMin, 最大値はMaxを使います。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};

// 最小値
int min = numbers.Min();
// 最大値
int max = numbers.Max();
Listの中から、条件に一致した最小値を取得

MinもMaxもLINQで提供されているので、他のLINQと繋いで条件に一致した最小値も以下のように一気に記載できます。
以下はMinのSampleですが、Maxも同様の書き方です。

// 50以上の数の中から最小値を取得するSample
List<int> numbers = new List<int> {11, 13, 32, 100, 203};

// 最小値
int min = numbers.Where(x => x > 50).Min();

上記の場合は、50以上の数の中での最小値を取得するので、100が取得できます。

Listの中から平均値を取得する「Average」

平均値を求めるにはAverageを利用します。
Averageはdouble型を返却します。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
double average = numbers.Average();

合計値を取得

合計値を求めるにはSumを利用します

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
int average = numbers.Sum();

// 以下はコレクションの例. プロパティにnumberを持ったオブジェクトリストのnumberの合計値を取得
int average = sameObject.Sum(x => x.number);

Listの中で条件に一致した合計数を求める「Count」

条件に一致した要素をカウントするにはCountを使います。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
// 10以上の数をカウント
int count = numbers.Count(x => x > 20);

List<T> を同じ値で埋める「Enumerable.Repeat」

同じ値で埋めるには、Repeatを使います。

// 10の要素100個分のList
List<int> numbers = Enumerable.Repeat(10, 100).ToList();
// "hoge"の要素100個分のList
List<string> hogeList = Enumerable.Repeat("hoge", 100).ToList();

上記のように、Repeatを用いる事で、同じ値を複数個用意し、その後ToListでListに変換しています。

List<T> を連続した値で埋める「Enumerable.Range」

連続した数値で埋めるには、Rangeを使います

// 1〜100の数値をセット
List<int> numbers = Enumerable.Range(1,100).ToList();

重複を排除した要素を抜き出す「Distinct」

重複を排除したい場合は、Distinctを使います。

// 11の重複が排除される
List<int> numbers = new List<int> {11, 11, 13, 32, 100, 203};
List<int> resultNumbers = numbers.Distinct().ToList();

並べ替え・sort

並べ替え・sortを行うにはOrderBy, OrderByDescendingを使用します。

List<int> numbers = new List<int> {11, 13, 203, 32, 100};
// 昇順
var resultNumbers = numbers.OrderBy(value => value);
// 降順
var resultNumbersDesc = numbers.OrderByDescending(value => value);

応用

応用といっても、これまでの延長線ですが、例えばゲームでHPが60未満のキャラクターに対して回復魔法を実行する機能を作ってみます。
ListとLINQを使ったSampleを強調したい為、すでにHpが減っている状態で、回復が発動したものを想像してください。

public class Charactor {
    public int ID;
    public string Name;
    public int Hp;
    public int Mp;
    public Charactor(int ID, string Name, int hp, int mp) {  
        this.ID   = ID;
        this.Name = Name;
        this.Hp = hp;
        this.Mp = mp;
    }
}

public class Sample : MonoBehaviour {

    List<Charactor> _partyList;

    void Awake() {
        PartySettings();
    }

    void Start () {
        _partyList = PartySettings();
        AutoHeal(_partyList);
    }

    List<Charactor> PartySettings() {
        List<Charactor> partyList = new List<Charactor> () {
            new Charactor(ID: 1, Name: "主人公", hp: 100, mp: 20),
            new Charactor(ID: 2, Name: "戦士", hp: 30, mp: 10),
            new Charactor(ID: 3, Name: "僧侶", hp: 20, mp: 40),
            new Charactor(ID: 4, Name: "魔法使い", hp: 50, mp: 50),
        };

        return partyList;
    }

    void AutoHeal(List<Charactor> partyList) {
        // パーティの中でHpが60未満のキャラを検索して回復
        var healTargets = partyList.Where(chara => chara.Hp <= 60);
        foreach (var chara in healTargets) {
            Debug.Log(chara.Name + " : " + chara.Hp);
            // 10回復
            chara.Hp += 10;
            Debug.Log(chara.Name + "のHPを10回復 -> " + chara.Hp);
        }
    }
}

さいごに

Listはそれ単体でも拡張性の高いデータ構造で大いに役に立ちますが、Linqと組み合わさる事で、簡単に要素へのアクセス・検索ができ、コードの可読性・生産性が大いに向上しますね。
私自身、Linqなどは使い始めたばかりですが、色々使って見て便利なやり方などが見つかったら引き続き記事に記載していこうと思います。

UITextFieldで入力判定を行う (UITextFieldTextDidChange)

はじめに

提供中のKeyHolderにて、UITextFieldで入力チェックを行っております。

f:id:project-unknown:20180305211123p:plain:w400

こんな感じでUITextFieldに何も入力されていない場合は、画面右上の保存を非表示にし、

f:id:project-unknown:20180305211335p:plain:w400

こんな感じでUITextFieldに入力されている場合は、画面右上の保存を表示しています。

やりかた

UITextFieldTextDidChangeを利用すると比較的簡単に実装できます。

以下は実装例です。

class SampleViewController: UIViewController {
    override func viewDidLoad() {
        NotificationCenter.default.addObserver(self, selector: #selector(SampleViewController.changeNotifyTextField(sender:)), name: NSNotification.Name.UITextFieldTextDidChange, object: nil)
    }
}

    public func changeNotifyTextField (sender: NSNotification) {
        guard let textView = sender.object as? UITextField else {
            return
        }
        if textView.text != nil {
            saveButton.isEnabled = textView.text != ""
        }
    }

senderの中に変更があったUITextFieldが入ってくるので、これをみて判断するだけです。

他の解決策

上記以外だったら、UITextFieldのDelegateを使うのも一手かもしれません、、、入力確定後の判定となるため、私はうまいやりかたを見つけられていません。

以下の例ではある程度の所は担保出来るのですが、上述通り予測変換等、入力確定前の情報がうまく取れません。

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

        var mutableText = ""
        if let textFieldString = textField.text {
            mutableText = mutableText + textFieldString
        }

        if mutableText == "" && string == "" {

        } else if (string == "") {
            mutableText = mutableText.substring(to: mutableText.endIndex)
        } else {
            mutableText = mutableText + string
        }

        saveButton.isEnabled = mutableText != ""
        
        return true
    }

ちなみに入力確認前の画面イメージは以下です。
f:id:project-unknown:20180305212128p:plain

UITextFieldTextDidChangeを使うと簡単に実現出来るので、こちらを使っておいたほうが良いかなと言う所感です。

UnityでGameObjectが画面上(カメラ内)に居るのか確認する

はじめに

UnityでGameObjectが画面(カメラ)の外に居るのか中に居るのかを確認したい時がママあります。
例えば、画面に表示された際に処理を行ったり、画面の外に行ったらGameObject自体を破棄したり。
今日はそのやり方の紹介です。

RendererのisVisibleを使う

カメラのviewPortで判断しても良いのですが、RendererのisVisibleを使うと便利です。

以下は、自分自身(GameObject)が画面上に表示されているのかのIsVisibleメソッドの例です。

public class SampleScript : MonoBehaviour {
    private Renderer _Renderer;
    
    void Start () {
        _Renderer = GetComponent<Renderer>();
    }

    void Update() {
        if (IsVisible()) {
            Debug.Lod("表示中");
        } else {
            Debug.Log("非表示中");
        }
    }

    public bool IsVisible() {
        return _Renderer.isVisible;
    }
}

これで、画面上に居るのか確認できます。