しめ鯖日記

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

【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)
    }
}

【Swift】ファイル書き込みで「Error Domain=NSCocoaErrorDomain Code=518 "指定されたURLタイプがサポートされていないため、ファイルを保存できませんでした。」エラーが出た時の対応

表題のエラーが出た時の対処法です。
今回は下のように画像をファイル保存をしようとしたら発生しました。

let path = "/xxx/yyy/zzz"
let image = UIImage()
let data = UIImageJPEGRepresentation(image, 80)
if let url = URL(string: path) {
    try? data?.write(to: URL(fileURLWithPath: path)!)
}

原因はURL(string: path)で生成したURLにスキーム(file://)が入ってない事でした。
下のようにURL(string: path)の代わりにURL(fileURLWithPath: path)を使うようにしたら保存する事ができました。

let path = "/xxx/yyy/zzz"
let image = UIImage()
let data = UIImageJPEGRepresentation(image, 80)
try? data?.write(to: URL(fileURLWithPath: path))

UIViewのサブクラスで`init?(coder aDecoder: NSCoder)`が必要と言われる理由を調べてみる

UIViewでinitializerを実装すると、init?(coder aDecoder: NSCoder)も実装するように言われます。
今回はinit?(coder aDecoder: NSCoder)と言われる理由などを調べてみます。

f:id:llcc:20170713231749p:plain

init?(coder aDecoder: NSCoder)とはなにか

init?(coder aDecoder: NSCoder)NSCodingプロトコルで定義されているメソッドです。
UIViewはNSCodingに準拠しているので、このメソッドも必須になっています。

public protocol NSCoding {
    public func encode(with aCoder: NSCoder)
    public init?(coder aDecoder: NSCoder)
}

NSCodingプロトコルとは

NSCodingプロトコルとは、そのクラスをアーカイブできるようにするものです。
NSCodingに準拠すれば、以下のようにNSKeyedArchiverを使ってData型への変換をする事ができます。

class MyClass: NSObject, NSCoding {
    override init() {
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        
    }
    
    func encode(with aCoder: NSCoder) {
        
    }
}

let data = NSKeyedArchiver.archivedData(withRootObject: MyClass())

UIViewもNSCodingに準拠しているのでアーカイブする事ができます。

let data = NSKeyedArchiver.archivedData(withRootObject: UIView())

NSCodingのメソッドはアーカイブ時、復元時に呼ばれます。

public protocol NSCoding {
    public func encode(with aCoder: NSCoder) // アーカイブする時に呼ばれる
    public init?(coder aDecoder: NSCoder) // アーカイブされたものを復元する時に呼ばれる
}

なぜinitにrequireが付くのか

initにrequireが付く件についてテストプロトコルを作って動かしてみました。
試したところ、protocolでinitを定義するとrequireになるようです。

protocol MyProtocol {
    init(test: Int)
}

class MyClass: MyProtocol {
    required init(test: Int) {
    }
}

optionalを付けるとどうなるかも試そうとしたのですが、コンパイルエラーになってしまいました。

@objc protocol MyProtocol {
    optional init(test: Int) // 'optional' cannot be applied to an initializerエラーになる
}

公式ドキュメントにも、requiredになると書いてありました。

The Swift Programming Language (Swift 4): Protocols

なぜUIViewサブクラスでinitを定義するとinit?(coder aDecoder: NSCoder)が必要になるのか

最後に、initを定義した途端にinit?(coder aDecoder: NSCoder)が必要になる理由についても調べてみました。

こちらはSwiftの仕様でした。
Swiftでは、initializerが1件もない時は親クラスのrequiredを再定義する必要がありません。
そのため、UIViewのサブクラスでinitを定義した途端にinit?(coder aDecoder: NSCoder)が必要になったようです。

class MyClass {
    init() {
    }
    
    required init(test: Int) {
    }
}

class MySubclass: MyClass {
}

NSNotification.Name.UIApplicationDidBecomeActiveの前半部が省略できる理由

表題の件について調べてみました。
NSNotification.Name.UIApplicationDidBecomeActiveはNotificationに渡す変数で、アプリ立ち上げ時に特定のメソッドを呼びたい時などに利用します。

// アプリがアクティブになった時にtestというメソッドを呼び出す
NotificationCenter.default.addObserver(
            self, selector: #selector(self.test), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)

今回はNSNotification.Name.UIApplicationDidBecomeActive.UIApplicationDidBecomeActiveと記述できる理由について調べてみました。

Notification(name: NSNotification.Name.UIApplicationDidBecomeActive)
// ↓ のように省略できる
Notification(name: .UIApplicationDidBecomeActive)

結論としては、UIApplicationDidBecomeActiveがクラス変数な為です。
Swiftでは下のようにクラス変数を省略する事ができます。

let test: MyClass = .Test
class MyClass {
    static let Test = MyClass()
}

Stored PropertyComputed Propertyに置き換えても省略できました。

let test: MyClass = .Test
class MyClass {
    static var Test: MyClass {
        return MyClass()
    }
}

Testという変数の型が違うとエラーになります。

// Member 'Test' in 'MyClass' produces result of type 'Int', but context expects 'MyClass' というエラーになる
let test: MyClass = .Test
class MyClass {
    static var Test: Int {
        return 1
    }
}

型が同じでも、他クラスの変数は使えません。

// これもエラーになる
let test: MyClass = .Test
class MyClass {
}

class MyClass2 {
    static var Test: MyClass {
        return MyClass()
    }
}

staticの代わりにclassを使って変数宣言しても問題なく動きます。

let test: MyClass = .Test
class MyClass {
    class var Test: MyClass {
        return MyClass()
    }
}

enumやstructでも同じような挙動になりました。

let test1: MyEnum = .Test
enum MyEnum {
    case test
    
    static var Test: MyEnum {
        return .test
    }
}

let test2: MyStruct = .Test
struct MyStruct {
    static var Test: MyStruct {
        return MyStruct()
    }
}