しめ鯖日記

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

SpriteKitで「fatal error: use of unimplemented initializer 'init(size:)'」が起きた時の対処法

表題のエラーが出た時の対処法です。
下のようにSpriteKitで独自Sceneを定義してそれを呼び出した時に発生しました。

let scene = StartScene()

class StartScene: SKScene {
    override init() {
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

スタックトレースを見たところ、親クラスのinitからinit(size: CGSize)を呼んでいました。

f:id:llcc:20170803125653p:plain

素直にinit(size: CGSize)を定義したら無事に動くようになりました。

let scene = StartScene()

class StartScene: SKScene {
    override init() {
        super.init()
    }
    
    override init(size: CGSize) {
        super.init(size: size)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

この時、init(size: CGSize)で渡されるサイズは1x1です。

f:id:llcc:20170803130109p:plain

Sceneを画面サイズに合わせたかったので、最終的に下のようにinit(size: CGSize)を使うようにしました。

let scene = StartScene(size: CGSize())

class StartScene: SKScene {
    override init(size: CGSize) {
        super.init(size: size)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Swiftでmp3の音を再生する(AVFoundation)

AVFoundationを使って音を鳴らしてみました。

まずは下のようにAssets.xcassetsにmp3ファイルを追加して下さい。

f:id:llcc:20170803160440p:plain

追加したデータは、下のようにNSDataAssetを使って取り出す事ができます。

let sound = NSDataAsset(name: "sound")
sound.data // → Data型、音データが入っている

最後に、今追加したデータを再生します。
再生は下のようにAVAudioPlayerを使って行います。

import UIKit
import AVFoundation

class ViewController: UIViewController {
    var player: AVAudioPlayer?
    
    func tapBtn() {
        if let sound = NSDataAsset(name: "sound") {
            player = try? AVAudioPlayer(data: sound.data)
            player?.play() // → これで音が鳴る
        }
    }
}

注意点ですが、下のようにplayerをローカル変数として定義すると音が再生されません。
再生前に変数が開放されるためだと思われます。

import UIKit
import AVFoundation

class ViewController: UIViewController {
    func tapBtn() {
        if let sound = NSDataAsset(name: "sound") {
            let player = try? AVAudioPlayer(data: sound.data)
            player?.play()
        }
    }
}

ALRTを使ってUIAlertControllerの記述をすっきりさせる

ALRTというライブラリを使って、UIAlertControllerを短く書いてみました。
とても便利なライブラリだったので、今後は積極的に使っていきたいと思います。

github.com

作った方の記事はこちらです。

qiita.com

インストール

CocoaPodsでインストールしました。
Carthageも対応しているようです。

target 'MyApp' do
  use_frameworks!

  pod "ALRT"
end

使い方

簡単なアラートなら、次のように一行で書くことができます。

import UIKit
import ALRT

class ViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        ALRT.create(.alert, title: "TEST!").addOK().show()
    }
}

f:id:llcc:20170807230646p:plain

複数のボタンを追加したい場合も下のように繋げて書く事ができます。

ALRT.create(.alert, title: "TEST!").addOK().addAction("action", handler: { _ in
    print("tap action")
}).show()

f:id:llcc:20170807230958p:plain

UIAlertActionStylecancelなボタンは、addCancelで追加できます。

ALRT.create(.alert, title: "TEST!").addCancel("OK").show()

textFieldにも対応しています。

ALRT.create(.alert, title: "TEST!").addTextField({ textField in
    textField.textColor = .blue
}).addCancel("OK").show()

f:id:llcc:20170807231235p:plain

【Swift, App Group】アプリ間でデータを共有する

アプリ間でデータを共有できる、App Groupを試してみました。
UserDefaultsのデータとファイルが共有可能です。
ただし自分が開発したアプリ同士でないと共有できないので注意が必要です。

まずは下のように、アプリを2つ作成します。
最初にMyApp1でデータの保存をしてみます。

f:id:llcc:20170730224732p:plain

まずはApp Groupを作成します。
XcodeのCapabilitiesからAppGroupをONにします。

f:id:llcc:20170730225239p:plain

有効化したら新しいApp Groupを作成します。
+ボタンから、AppGroupを追加してください。

f:id:llcc:20170730225258p:plain

続けてデータ保存部分の実装をします。
データ保存は下のようにsuiteName付きでUserDefaultsを初期化して行います。

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        let userDefaults = UserDefaults(suiteName: "group.com.myapp")
        userDefaults?.set(10, forKey: "testKey")
        userDefaults?.synchronize()
        print(userDefaults?.integer(forKey: "testKey")) // → 10
        
        return true
    }
}

MyApp1でデータを保存したので、次はMyApp2でそのデータを取得してみます。
MyApp2を起動したら、MyApp1のときと同様にCapabilitiesからAppGroup有効化と先ほど作成したApp Group選択を行います。

f:id:llcc:20170730225916p:plain

取得は下の通りです。
無事にMyApp1で保存したデータを取得できました。

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        let userDefaults = UserDefaults(suiteName: "group.com.myapp")
        print(userDefaults?.integer(forKey: "testKey")) // → 10
        
        return true
    }
}

それとsuiteNameは、Typoしてもエラーになったりnilを返したりしないので気をつける必要があります。

let userDefaults = UserDefaults(suiteName: "group.com.myapp.wrong")
userDefaults?.set(12, forKey: "testKey")
userDefaults?.synchronize()

SceneKitで利用できる図形

SceneKitで利用できる図形を実際に配置して見ました。
対象クラスは下ドキュメントを参考にしました。

Built-in Geometry Types | Apple Developer Documentation

SCNBox

立方体を生成するクラスです。
x, y, z を変更できます。

f:id:llcc:20170727225758p:plain

SCNFloor

名前の通り床として利用できる図形です。
サイズは無限で、この上に様々なオブジェクトを配置します。

f:id:llcc:20170727225237p:plain

SCNCapsule

薬カプセルのような形を表します。
高さと太さを調整する事ができます。

f:id:llcc:20170727225955p:plain

SCNCone

円錐を表す事ができるクラスです。
上下の半径と高さを調整できます。

f:id:llcc:20170727230215p:plain

SCNCylinder

円柱を表すクラスです。
高さと半径を調整できます。

f:id:llcc:20170727230358p:plain

SCNPlane

こちらは高さと横幅だけを持つ四角形です。
奥行きはありません。

f:id:llcc:20170727230514p:plain

SCNPyramid

ピラミッド型の図形を表現できます。
高さ、横幅、縦幅を変更できます。

f:id:llcc:20170727230656p:plain

SCNSphere

こちらは球体です。
半径を調整する事ができます。

f:id:llcc:20170727230807p:plain

SCNTorus

ドーナツ型の図形を表します。
半径と、内側の穴の大きさを調整できます。

f:id:llcc:20170727230944p:plain

SCNTube

真ん中に穴が空いた円柱を表します。
SCNTorus同様に半径と内側の円の大きさを調整できます。

f:id:llcc:20170727231045p:plain

【iOS】ファイルから読んだUIImageが表示されない問題対応

ファイルから画像を読み込むアプリでうまく画像表示できない事がありました。

class ViewController: UIViewController {
    var imageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 画像をファイルに保存
        let path = "\(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])/test"
        if let originalImage = UIImage(named: "image") {
            let path = "\(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])/test"
            let data = UIImagePNGRepresentation(originalImage)
            try? data?.write(to: URL(fileURLWithPath: path), options: [.atomic])
        }
        
        // 画像をファイルから読み込み
        let image = UIImage(contentsOfFile: path)
        imageView = UIImageView(image: image)
        imageView.backgroundColor = UIColor.white
        view.addSubview(imageView)
    }
}

下は正常な表示です。

f:id:llcc:20170717221827p:plain

画像読み込みが失敗したのは下のようなケースです。
画像読み込み後に元ファイルを削除すると、画像が表示されなくなりました。

class ViewController: UIViewController {
    var imageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 省略…
        
        // ファイルを削除
        try? FileManager.default.removeItem(atPath: path)
    }
}

次のように何も表示されなくなります。

f:id:llcc:20170717222015p:plain

ファイルの削除をviewDidAppearにしたら問題なく画像表示できました。

class ViewController: UIViewController {
    var imageView = UIImageView()
    
    // 省略…
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let path = "\(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])/test"
        try? FileManager.default.removeItem(atPath: path)
    }
}

viewWillAppearでファイルを削除した場合は画像表示できませんでした。
おそらく画面描画のタイミングでファイルを読み込んでいるため、画面描画前であるviewWillAppearで削除すると画像読み込みが失敗するのだと思います。

class ViewController: UIViewController {
    var imageView = UIImageView()
    
    // 省略…
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        let path = "\(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])/test"
        try? FileManager.default.removeItem(atPath: path)
    }
}

ちなみに公式ドキュメントによると、ファイルのリロード処理が走る事があるようです。
そのため、viewDidAppearで削除してもリロード処理が走ったら画像が表示されない可能性があります。

init(contentsOfFile:) - UIImage | Apple Developer Documentation

Discussion This method loads the image data into memory and marks it as purgeable. If the data is purged and needs to be reloaded, the image object loads that data again from the specified path.

対策

今回は、Data型を経由する事で対策しました。
次のようにファイルからData形式で読み込んで、それをUIImageに渡しました。

class ViewController: UIViewController {
    var imageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 画像をファイルに保存
        let path = "\(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])/test"
        if let originalImage = UIImage(named: "image") {
            let path = "\(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])/test"
            let data = UIImagePNGRepresentation(originalImage)
            try? data?.write(to: URL(fileURLWithPath: path), options: [.atomic])
        }
        
        // 画像をファイルから読み込み
        if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
            let image = UIImage(data: data)
            imageView = UIImageView(image: image)
            imageView.backgroundColor = UIColor.white
            view.addSubview(imageView)
        }
        
        // ファイルを削除
        try? FileManager.default.removeItem(atPath: path)
    }
}

これで無事に画像表示できるようになりました。

f:id:llcc:20170717223501p:plain

【Swift】UIImagePickerControllerでカメラロールから写真を取得

UIImagePickerControllerを使ったカメラロールの操作を試してみました。

ImagePickerの表示

まずはカメラロールの操作の為に、Info.plistにNSPhotoLibraryUsageDescriptionというキーを追加します。
ここにはカメラロールの利用目的を書きます。

f:id:llcc:20170717170608p:plain

ここに書かれた文言が、アクセス許可ポップアップに表示されます。

f:id:llcc:20170717170658p:plain

ImagePicker(画像選択画面)の表示は下の通りです。

import UIKit

class ViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let c = UIImagePickerController()
        present(c, animated: true)
    }
}

アプリを立ち上げると、下のように画像選択画面が表示されます。

f:id:llcc:20170717170908p:plain

ImagePickerは、画像選択時・キャンセルボタン押下時のイベントを取る事ができます。
イベント取得は下のようにUIImagePickerControllerDelegateを使います。

class ViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let c = UIImagePickerController()
        c.delegate = self // 今回追加
        present(c, animated: true)
    }
}

extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        // キャンセルボタンを押された時に呼ばれる
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        // 写真が選択された時に呼ばれる
    }
}

ユーザーがカメラロールへのアクセスを許可しているかどうかは下メソッドで取得できます。

PHPhotoLibrary.requestAuthorization { status in
    switch status {
    case .authorized: print("authorized")
    case .denied: print("denied")
    case .notDetermined: print("NotDetermined")
    case .restricted: print("Restricted")
    }
}

不許可の場合にImagePickerを立ち上げると、下のような表示になります。

f:id:llcc:20170717171732p:plain

画像を取得する

選択画像の取得は、UIImagePickerControllerDelegateのimagePickerController:didFinishPickingMediaWithInfoメソッドで行います。
引数のinfoにUIImageが入っているので、下のように取り出します。

extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        let image = info[UIImagePickerControllerOriginalImage] as? UIImage
    }
}

画像の他にも、メディアのタイプとメディアのURLを取得できます。

extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        
        let mediaType = info[UIImagePickerControllerMediaType] as? String // → public.image
        let imageUrl = info[UIImagePickerControllerReferenceURL] as? URL // → assets-library://asset/asset.JPG?id=XXXX&ext=JPG
    }
}

UIImagePickerControllerのオプション

デフォルトでは、UIImagePickerControllerは画像しか取得できません。
下のようにmediaTypesを変更することで動画も扱えるようになります。

class ViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let c = UIImagePickerController()
        c.mediaTypes = ["public.image", "public.movie"]
        present(c, animated: true)
    }
}

取扱可能なメディアはavailableMediaTypesメソッドで確認できます。

print(UIImagePickerController.availableMediaTypes(for: .photoLibrary))

UIImagePickerControllerのsourceTypeを.cameraにすればカメラ撮影での画像取得もできます。

class ViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let c = UIImagePickerController()
        c.sourceType = .camera
        present(c, animated: true)
    }
}