20191008のiOSに関する記事は10件です。

<iOS> 「iOS10以上」と「iOS9.3.5以下」を互換性を持たせてCoreDataを使う

目的

今回、初めてXcodeを触って、アプリを作ってみた。
ただ、XcodeとSwiftのバージョンが新しいのに、実機で持っていたのが「iOS9.3.5」だった。仮想デバイスですら「iOS10」なのにである。

デバッグしてみると、悲しいことにいろんなところで調べたことが古くて使えないよ!と言われ、困り果て、何とかして両方使えるように共存できないか探ってみたら、何と似たようなことで困っていた人が記事にしてくれていたので、何とか助かった。

調べても、問題が解決しない記事が多かったので、今回自分でまとめることにした。備忘録。

何が問題?

「iOS10以降」と「それ以下」の違いが大きく関わっている。

 ・CoreDataの宣言の仕方が割と違う。
   → そもそも、AppDelegateで宣言されているものが違うので、使えるメソッドも違う。

 ・AppDelegateで宣言されているものが違う
   → 前述したように、宣言されているものが違うので、Contextの宣言方法もまるで違う。

※因みに、古いXcodeで作ればいいやんと思ってやってみたけど、macOS Mojaveでは、うまくXcodeの共存(つまり、Xcodeのバージョンが古いのと新しいのを両方とも置くこと)ができない。起動時にエラーが出る。(StackOverFlowでも探したけど、みんな同じことが起こって諦めている感じだった。)

対応方法

ここでやりたいのは「共存」。どっちでも使えるようにしたいと考え、以下のように対応した。

  • 古いXcodeで宣言されているコードを、新しい方に移植する。

無ければ入れれば良い話で、そこからメソッドを呼び出せばいい。

古いバージョンのXcodeで宣言されているAppDelegateのメソッドを、新しいXcodeのAppDelegateに記述すれば、古い方からもそのバージョンにあったContextを呼び出せるという考え。

今回作ったアプリ

Simulator Screen Shot - iPhone 7 - 2019-10-08 at 22.23.32.png
Simulator Screen Shot - iPhone 7 - 2019-10-08 at 22.30.54.png
Simulator Screen Shot - iPhone 7 - 2019-10-08 at 22.31.40.png
Simulator Screen Shot - iPhone 7 - 2019-10-08 at 22.31.45.png

AppDelegate

AppDelegate.swift
import UIKit
import CoreData

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

/*ここまでは、新しいXCodeでは自動的に定義されている。
-------------------------------------------------------------------
ここから下が、古いXcodeからの移植 */

    lazy var applicationDocumentsDirectory: NSURL = {
        let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return urls[urls.count-1] as NSURL
    }()

    lazy var managedObjectModel: NSManagedObjectModel = {
        // The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
        //forResource: には自分のAppの名前を入れる。
        let modelURL = Bundle.main.url(forResource: "MyAppName", withExtension: "momd")!
        return NSManagedObjectModel(contentsOf: modelURL)!
    }()

    lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
        // The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
        // Create the coordinator and store
        let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
        //appendingPathComponentの引数には、データベース名.sqlite
        let url = self.applicationDocumentsDirectory.appendingPathComponent("MyDB.sqlite")
        var failureReason = "There was an error creating or loading the application's saved data."
        do {
            try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
        } catch {
            // Report any error we got.
            var dict = [String: AnyObject]()
            dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject?
            dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject?

            dict[NSUnderlyingErrorKey] = error as NSError
            let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
            // Replace this with code to handle the error appropriately.
            // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
            abort()
        }

        return coordinator
    }()

    lazy var managedObjectContext: NSManagedObjectContext = {
        // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
        let coordinator = self.persistentStoreCoordinator
        var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        managedObjectContext.persistentStoreCoordinator = coordinator
        return managedObjectContext
    }()

/*ここまでをまずは移植する。
------------------------------------------------------------------------
*/
    // MARK: - Core Data Saving support

    @available(iOS 10.0, *)
    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }

    //iOS9以下のCoreData_Saveメソッド これも移植したもの。
    func saveContext9 () {
        if managedObjectContext.hasChanges {
            do {
                try managedObjectContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
                abort()
            }
        }
    }
}

Contextの宣言

ViewController.swift
  func getData(){
        //ここでiOSのバージョンによって処理が変わる。
        if #available(iOS 10.0, *) {
            //Contextの宣言(iOS10)
            let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

            do{
                sample = try context.fetch(Sample.fetchRequest())
            }catch{
                print("Error")
            }
        }else{
            //Contextの宣言(iOS9)
            let context = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext

            do{
                sample = try context.fetch(Sample.fetchRequest())
            }catch{
                print("Error")
            }   
        } 
    }

CoreData-Save編-

Add.swift
        if #available(iOS 10.0, *) {
            let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

            let defaultAction:UIAlertAction = UIAlertAction(title: "OK!", style: UIAlertAction.Style.default) {  (action: UIAlertAction!) -> Void in

                let sample = Sample(context: context)

                sample.id = Int32(self.AutoIncrements())
                sample.name = self.nameField.text
                sample.mail = self.mailField.text
                sample.pass = self.passField.text

                (UIApplication.shared.delegate as! AppDelegate).saveContext()

                self.navigationController!.popViewController(animated: true)
            }

             alert.addAction(defaultAction)
        } else {
            // Fallback on earlier versions
            let context9:NSManagedObjectContext = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext

            let defaultAction:UIAlertAction = UIAlertAction(title: "OK!", style: UIAlertAction.Style.default) {  (action: UIAlertAction!) -> Void in
             //ここからが新しいXcodeと違うところ
             let entity = NSEntityDescription.entity(forEntityName: "Sample", in: context9)
             let sample = NSManagedObject(entity: entity!, insertInto: context9) as! Sample

             sample.id = Int32(self.AutoIncrements())
             sample.name = self.nameField.text
             sample.mail = self.mailField.text
             sample.pass = self.passField.text

             (UIApplication.shared.delegate as! AppDelegate).saveContext9()

             self.navigationController!.popViewController(animated: true)
            }
          alert.addAction(defaultAction)
        }

終わりに

まだまだ記録できていないところはあるけれど、とりあえずここまで。
暇なときに追記していきます。

参考サイト

-- xcode8でCoreData使うときにiOS9以前も対応に含めるときに --
https://qiita.com/fujiwarawataru/items/12903b6e8fe42d6464f2

ここでヒントをもらいました。@fujiwarawataruさん。この記事を書いていただき感謝です。助かりました。

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

<iOS:Swift> 「iOS10以上」と「iOS9.3.5以下」を互換性を持たせてCoreDataを使う

目的

今回、初めてXcodeを触って、アプリを作ってみた。
ただ、XcodeとSwiftのバージョンが新しいのに、実機で持っていたのが「iOS9.3.5」だった。仮想デバイスですら「iOS10」なのにである。

デバッグしてみると、悲しいことにいろんなところで調べたことが古くて使えないよ!と言われ、困り果て、何とかして両方使えるように共存できないか探ってみたら、何と似たようなことで困っていた人が記事にしてくれていたので、何とか助かった。

調べても、問題が解決しない記事が多かったので、今回自分でまとめることにした。備忘録。

何が問題?

「iOS10以降」と「それ以下」の違いが大きく関わっている。

 ・CoreDataの宣言の仕方が割と違う。
   → そもそも、AppDelegateで宣言されているものが違うので、使えるメソッドも違う。

 ・AppDelegateで宣言されているものが違う
   → 前述したように、宣言されているものが違うので、Contextの宣言方法もまるで違う。

※因みに、古いXcodeで作ればいいやんと思ってやってみたけど、macOS Mojaveでは、うまくXcodeの共存(つまり、Xcodeのバージョンが古いのと新しいのを両方とも置くこと)ができない。起動時にエラーが出る。(StackOverFlowでも探したけど、みんな同じことが起こって諦めている感じだった。)

対応方法

ここでやりたいのは「共存」。どっちでも使えるようにしたいと考え、以下のように対応した。

  • 古いXcodeで宣言されているコードを、新しい方に移植する。

無ければ入れれば良い話で、そこからメソッドを呼び出せばいい。

古いバージョンのXcodeで宣言されているAppDelegateのメソッドを、新しいXcodeのAppDelegateに記述すれば、古い方からもそのバージョンにあったContextを呼び出せるという考え。

今回作ったアプリ

Simulator Screen Shot - iPhone 7 - 2019-10-08 at 22.23.32.png
Simulator Screen Shot - iPhone 7 - 2019-10-08 at 22.30.54.png
Simulator Screen Shot - iPhone 7 - 2019-10-08 at 22.31.40.png
Simulator Screen Shot - iPhone 7 - 2019-10-08 at 22.31.45.png

AppDelegate

AppDelegate.swift
import UIKit
import CoreData

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

/*ここまでは、新しいXCodeでは自動的に定義されている。
-------------------------------------------------------------------
ここから下が、古いXcodeからの移植 */

    lazy var applicationDocumentsDirectory: NSURL = {
        let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return urls[urls.count-1] as NSURL
    }()

    lazy var managedObjectModel: NSManagedObjectModel = {
        // The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
        //forResource: には自分のAppの名前を入れる。
        let modelURL = Bundle.main.url(forResource: "MyAppName", withExtension: "momd")!
        return NSManagedObjectModel(contentsOf: modelURL)!
    }()

    lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
        // The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
        // Create the coordinator and store
        let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
        //appendingPathComponentの引数には、データベース名.sqlite
        let url = self.applicationDocumentsDirectory.appendingPathComponent("MyDB.sqlite")
        var failureReason = "There was an error creating or loading the application's saved data."
        do {
            try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
        } catch {
            // Report any error we got.
            var dict = [String: AnyObject]()
            dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject?
            dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject?

            dict[NSUnderlyingErrorKey] = error as NSError
            let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
            // Replace this with code to handle the error appropriately.
            // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
            abort()
        }

        return coordinator
    }()

    lazy var managedObjectContext: NSManagedObjectContext = {
        // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
        let coordinator = self.persistentStoreCoordinator
        var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        managedObjectContext.persistentStoreCoordinator = coordinator
        return managedObjectContext
    }()

/*ここまでをまずは移植する。
------------------------------------------------------------------------
*/
    // MARK: - Core Data Saving support

    @available(iOS 10.0, *)
    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }

    //iOS9以下のCoreData_Saveメソッド これも移植したもの。
    func saveContext9 () {
        if managedObjectContext.hasChanges {
            do {
                try managedObjectContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
                abort()
            }
        }
    }
}

Contextの宣言

ViewController.swift
  func getData(){
        //ここでiOSのバージョンによって処理が変わる。
        if #available(iOS 10.0, *) {
            //Contextの宣言(iOS10)
            let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

            do{
                sample = try context.fetch(Sample.fetchRequest())
            }catch{
                print("Error")
            }
        }else{
            //Contextの宣言(iOS9)
            let context = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext

            do{
                sample = try context.fetch(Sample.fetchRequest())
            }catch{
                print("Error")
            }   
        } 
    }

CoreData-Save編-

Add.swift
        if #available(iOS 10.0, *) {
            let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

            let defaultAction:UIAlertAction = UIAlertAction(title: "OK!", style: UIAlertAction.Style.default) {  (action: UIAlertAction!) -> Void in

                let sample = Sample(context: context)

                sample.id = Int32(self.AutoIncrements())
                sample.name = self.nameField.text
                sample.mail = self.mailField.text
                sample.pass = self.passField.text

                (UIApplication.shared.delegate as! AppDelegate).saveContext()

                self.navigationController!.popViewController(animated: true)
            }

             alert.addAction(defaultAction)
        } else {
            // Fallback on earlier versions
            let context9:NSManagedObjectContext = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext

            let defaultAction:UIAlertAction = UIAlertAction(title: "OK!", style: UIAlertAction.Style.default) {  (action: UIAlertAction!) -> Void in
             //ここからが新しいXcodeと違うところ
             let entity = NSEntityDescription.entity(forEntityName: "Sample", in: context9)
             let sample = NSManagedObject(entity: entity!, insertInto: context9) as! Sample

             sample.id = Int32(self.AutoIncrements())
             sample.name = self.nameField.text
             sample.mail = self.mailField.text
             sample.pass = self.passField.text

             (UIApplication.shared.delegate as! AppDelegate).saveContext9()

             self.navigationController!.popViewController(animated: true)
            }
          alert.addAction(defaultAction)
        }

終わりに

まだまだ記録できていないところはあるけれど、とりあえずここまで。
暇なときに追記していきます。

参考サイト

-- xcode8でCoreData使うときにiOS9以前も対応に含めるときに --
https://qiita.com/fujiwarawataru/items/12903b6e8fe42d6464f2

ここでヒントをもらいました。@fujiwarawataruさん。この記事を書いていただき感謝です。助かりました。

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

SwiftUI(View)とUIKit(UIView)と同じ画面にだしてsinkさせてみる

今回試してみたこと

今回はUIKitで作成したViewControllorとSwiftUIで作成したViewを一緒に表示させることに挑戦してみたいと思います。

おそらく新規プロジェクトでなければ、SwiftUIに移行することになったときに必ずやることになると思ったからです。


図にするとこんな感じ

スクリーンショット 2019-10-08 9.11.28.png

とりあえず共有するデータを定義

final class MyData: ObservableObject {
    @Published var name: String = ""
}

そして、SwiftUIでViewを定義

struct TestView: View {
    @EnvironmentObject var myData: MyData
    var body: some View {
        HStack {
            Text(myData.name)
                .frame(minWidth: 100)
            Button(action: {
                self.myData.name = Int(arc4random_uniform(50000)).description
            }) {
                Text("タップして乱数生成")
            }.frame(minWidth: 100)
        }
    }
}

frameのminWidthやminHeightはよく使うことになりそうです。

これでボタンをタップしたら乱数を生成してTextで表示させるシンプルなViewが出来ました。


こんな感じ

スクリーンショット 2019-10-08 9.20.26.png

次はUIViewを作る

class MyTestUIView: UIView {
    /// 共有するデータ
    var data: MyData  

    /// 今回はコードでviewを生成します。MyDataをこのときに渡してます。
    init(frame: CGRect, data: MyData) {
        self.data = data
        super.init(frame: frame)
        // UIKitで生成したViewをわかりやすくするためにyellowにしました
        backgroundColor = UIColor.yellow

        // textFieldに値を反映し、修正も行います
        let textField = UITextField(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        addSubview(textField)
        textField.borderStyle = .roundedRect

        // (1) textFieldからのデータInputを処理する
        NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification,
                                               object: nil,
                                               queue: OperationQueue.main) { notification in
                                                guard let text = (notification.object as? UITextField)?.text else { return }
                                                data.name = text
        }
        // (2) SwiftUIからInputが発生したときにTextFieldにも反映させる
        _ = data.$name.receive(on: DispatchQueue.main).sink() {
            textField.text = $0
        }
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

確認するところは2点あります

(1) textFieldからのデータInputを処理する

NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification,
                                       object: nil,
                                       queue: OperationQueue.main) { notification in
    guard let text = (notification.object as? UITextField)?.text else { return }
    data.name = text
}

TextFieldの通知を活用してtextFieldの入力が発生したときのイベントをハンドリングしてみました。
textFieldのtextをdata.nameに代入します。

もうこれでSwiftUIで定義したViewに反映されます。

HStack {
    Text(myData.name)
    .frame(minWidth: 100)

こちらのmyData.name@Publishedのおかげてデータが変更されたらTextが即時に更新されます

(2) SwiftUIからInputが発生したときにTextFieldにも反映させる

_ = data.$name.receive(on: DispatchQueue.main).sink() {
    textField.text = $0
}

@Publishedのnameの変更をmainスレッドでreceiveして、sinkでクロージャーを登録してデータが変わったことを知らせてもらっています。


とりあえずやってみたいことは出来ました。

シンプルにしたかったのでtextFieldの変更が入るたびにデータが更新されtextFieldもまた更新されるといった、無駄なことをやってるソースコードにも見えますがw

例えばメモ帳アプリで
上部はUIKItのカレンダーライブラリーで暦の表示
下部はSwiftUIで選択された日付のデータを表示
presentされた画面でイベントが登録されたら両方に反映

とかがあり得るのかな?

他の書き方でも似たようなことは出来ると思うのでなにかあればぜひコメントもお願いします!

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

アプリのCI/CD関連サービス有名どころ調べてみた(簡易版)

Yoki(@enyyokii)と申します。

今回はCI/CD関連サービスの有名なものを調べてみたので簡単にまとめてみました。
AppCenterが思ったより便利でした ☺️

CI/CDサービス

役割 サービス名 機能 対象 料金 備考
ビルドツール fastlane ビルド自動化
テスト実行
証明書の管理
スクリーンショットの生成
iOS: 主流
Android: △
全ての機能が使用できるわけではない
google playへのリリースやスクリーンショットの保存が可能
しかし、gradleがビルドツールとしての役割を担うため、fastlaneそのものへのありがたみは少ない。
無料 アプリのタスク実行ツール
BitriseなどのCIツールと併用しローカルで実行しないように生産効率あげることが多い
CI/CD App Center ビルド時間:
(無料)240 ビルド時間 (分)/月
ビルドごとに 30 分まで
テスト:無制限の起動テスト
Android
iOS
macOS
Windows(UWP)
Xamarin
基本無料
無制限のビルド時間+複数のビルド同時実行: ¥4480円/月
UIテストを好きな構成で実行: ¥11088円/月
ViewController2つのWebView表示する簡易アプリでビルド時間は10分程(初回は時間かかるかも)
これはすぐに超えそうなので¥4480/月は契約した方がいいかも?
CI/CD Bitrise 設定方法: GUI(WEB)、YAML
ビルド時間制限約8分
同時実行数:
(無料)x1
(有料)x2〜x18
Android
iOS
Xamarin
無料: 0
有料: $40/month〜(年契約もあり)
アプリに特化している
iOSDCへ参加するなど日本市場へ注力している。日本語記事も見つけやすい。
CI/CD Circle CI 設定方法: YAML モバイルアプリ
Webアプリ
Linuxでのビルド: コンテナ数1 → 無料 ,
コンテナ数増やすごとに$50ずつかかる
macOSでのビルド: $39/month〜
ビルドコンテナメモリ上限: 4G
2コンテナ以上利用する場合は、ビルド時間の制限はなくなる
アカウント登録はgithub連携必須
  • AppCenterのみ試してみたが、30分もあればpushをトリガーに、ビルド、単体テスト、配布までを用意に自動化できる。ストアへのアップロードもできる。

  • プロダクトをまたいでテストアプリを触るケースがあるので配布ツールは統一したいかも

  • iOSの場合、ワークフローはfastlaneにまとめておき、CI/CDツールでfastfileを実行するようにしておけばツールを変更した際の移行が楽(ZOZOの事例より)

  • 複数のプラットフォームで同じようにワークフローを管理したいのであれば、Bitriseは選択肢から外れそう。

参考

0から始めるiOS自動化
AndroidのCIサービスをCircleCIからBitriseへ移行しました
iOSで構築しているCIのWorkflow紹介
Visual Studio App Centerで始めるCI/CD(iOS)
署名なしでiOSアプリのビルド&単体テスト〜AppCenter編〜

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

iOS13 UISegmentControlのBeforeAfterを見てみる

概要

iOS13からUISegmentedControlの見た目と動きが大きく変更になりました。

default.png

これまではButtonが複数あるタブっぽい動きでしたが、UISwitchのような動きに変わりましたね。
iOS13では他の新機能が目立って取り上げられていたので、UIKitの見た目変更があると全く知らなくて
開発中アプリの動作確認をしていたら、激変していてびっくりしました汗
実装する上でも変更点があったので備忘録としてまとめていきます。

変更点

iOS7からはUISegmentControlの色の指定はtintColorでしていましたが、
iOS13以降はこれが使えなく、代わりにselectedSegmentTintColorが追加されました。
機能的には全く同じ。
そう、 プロパティ名が長くなっただけ←

どちらもアプリで対応する場合はavailableを使用してバージョン管理が必要です。

SampleViewController.swift
import UIKit

class SampleViewController: UIViewController
{
    @IBOutlet weak var segmentControl: UISegmentedControl!

    override func viewDidLoad()
    {
        super.viewDidLoad()

        // 背景色
        segmentControl.backgroundColor = .white
        // ベースカラー
        if #available(iOS 13.0, *) {
          segmentControl.selectedSegmentTintColor = .red
        } else {
          segmentControl.tintColor = .red
        }
        // 通常時の文字色
        segmentControl.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], for: .normal)
        // 選択時の文字色
        segmentControl.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], for: .selected)
    }
}

colored.png

ちなみに、背景色に白色を指定すると影がついたようなグレーになってしまいます。。
これはそういう仕様のようで色々頑張ってみましたが背景を完全な白にできませんでした。。
どうしてもしたい場合は画像を背景にするしかないのかな?汗

また、iOS12以前のSegmentControlのように外枠をつけてみました。

SampleViewController.swift
// 上記のコードに追記
segmentControl.layer.borderColor = UIColor.red.cgColor
segmentControl.layer.borderWidth = 1
segmentControl.layer.cornerRadius = 4

bordered.png

ちょっと外枠と内側のタブの間に隙間ができるんですね。なるほど。。
iOS12以前のものはパッと見変化がないですが、cornerRadiusの値によって角丸のところが色が固まっているみたいになりました。。
下手に外枠はつけない方が良さそうですかね汗

参考

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

xcode11でシミュレーターに機種を追加する方法

xcode11にアップデートしたところ、シミュレーターの一覧からSEが消えていたので追加した
storyboardのDeviceからiPhoneXSが消えたのは、iPhone11 proと同じやぞそんくらい覚えろやということですか??

機種の追加方法

Add Additional Simulators...を選択
スクリーンショット 2019-10-08 19.30.36.png

左下の+ボタンを選択し、必要なDevice Type を選択してcleate
スクリーンショット 2019-10-08 19.22.30.png

追加できました

以上

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

Flutterウィークリー #78

Flutterウィークリーとは?

FlutterファンによるFlutterファンのためのニュースレター
https://flutterweekly.net/

この記事は#78の日本語訳です
https://mailchi.mp/flutterweekly/flutter-weekly-78

※Google翻訳を使って自動翻訳を行っています。翻訳に問題がある箇所を発見しましたら編集リクエストを送っていただければ幸いです。

読み物&チュートリアル

Flutter内部

https://www.didierboelens.com/2019/09/flutter-internals/


これは必読です。 Didier Boelensは、 Flutter描画サイクルの内部が詳細にどのように機能するかを説明します。

ListViewで無限スクロールを実装する

https://codinglatte.com/posts/flutter/listview-infinite-scrolling-in-flutter/


Maina Wycliffeは、一番下までスクロールするときに、より多くのコンテンツをロードするリストを作成する方法を示します。

Flutterメッセージングアプリを構築する-パートI:プロジェクトの構造

https://medium.com/@MiBLT/building-a-messaging-app-in-flutter-part-i-project-structure-7d6db38783a5


Miguel Beltranは、顧客がキャンセルし、共有することに同意し、そこで行われた作業の文書化を開始した後、ほぼ完全なプロジェクトを共有します。

Flutterアクセシビリティ対応アプリの開発とテスト

https://medium.com/flutter-community/developing-and-testing-accessible-app-in-flutter-1dc1d33c7eea


Flutterアプリでのアクセシビリティの実装に取り組んだ後、Darshan Kawarは彼がそれについて学んだことを説明します。

プロバイダーを使用してFlutter動的テーマを作成する方法

https://medium.com/better-programming/how-to-create-a-dynamic-theme-in-flutter-using-provider-e6ad1f023899


Divyanshu Bhargavaによるこのチュートリアルで、Providerを使用して実行時にアプリのテーマを変更することを学びます。

GameKitとGoogleプレイサービスを統合する方法

https://medium.com/@abedalkareemomreyh/how-to-integrate-gamekit-and-google-play-services-flutter-4d3f4a4a2f77


Abedalkareem Omreyhが、 FlutterアプリでiOS用GameKitとAndroid用Goople Play Servicesを使用する方法を説明します。

BackdropFilterとImageFilterを使用してFlutterぼかし効果を作成する方法

https://medium.com/fluttervn/how-to-make-blur-effect-in-flutter-using-backdropfilter-imagefilter-559ffd8ab73


Bui Minh Trietによるこのチュートリアルで、アプリにぼかし効果を適用する方法を学びます。

Flutterプロシージャルテクスチャ

https://medium.com/@av.codes/procedural-textures-with-flutter-efcf546cd1fc


あなたは芸術に興味がありますか? Ivan CherepanovはFlutter Perlinノイズのような手続き型テクスチャを作成する方法を説明します。

ダイアグラムでのプロバイダーの理解—パート1:値の提供

https://medium.com/flutter-community/understanding-provider-in-diagrams-part-1-providing-values-4379aa1e7fd5


Joseph T. Lappによるアプリでのプロバイダーの使用に関する3部構成の記事。記事でリンクされているパート2と3。

FlutterがNubankでのモバイル開発のスケーリングに役立つと考える理由

https://medium.com/building-nubank/https-medium-com-freire-why-nubank-chose-flutter-61b80b568772


NubankのAlexandre Freireは、開発プラットフォームを採用するために行ったプロセスについてこの優れた記事を書きました。ネタバレ: Flutter勝ちます。

FlutterでバーコードとQRコードを操作する

https://medium.com/flutter-community/working-with-barcodes-and-qr-codes-in-flutter-fbb3a2d4ede7


FlutterアプリからのバーコードとQRコードのスキャンに関するRitesh Sharmaのチュートリアル。

ビデオ&メディア

プロバイダーでのデータ管理

https://www.youtube.com/watch?v=nZvELm3QnXc&feature=youtu.be


Raja Yoganによるこのビデオチュートリアルで、プロバイダーでデータを処理する方法を学びます。

Hive( Flutter Tutorial)– Pure Dart軽量で高速なNoSQLデータベース

https://www.youtube.com/watch?v=R1GSrrItqUs&feature=youtu.be


軽量でありながら強力なデータベースであるHiveの使用に関するチュートリアル。開発が容易で、デバイス上でも高速に実行されます。

ListTile( Flutter Widget of the Week)

https://www.youtube.com/watch?v=l8dj0yPBvgQ&list=PLjxrf2q8roU23XGwz3Km7sQZFTdB996iG&index=55&t=0s


ListTileはリストアイテムのマテリアルデザインパターンを実装するため、リストアイテムの内容について心配する必要はありません。

Flutterデスクトップアプリを作成する方法

https://www.youtube.com/watch?v=cMK6qxmTdaM


このビデオでは、LinuxおよびWindowsでFlutterを実行する方法について説明します。現在の状態は何ですか?試してみたい場合は何を達成できますか?

BLoCパターンでFlutterコードを設計する方法

https://www.youtube.com/watch?v=NCJZ_T8FiUo


BLoCパターンに従って、可能な限り最も簡単かつ効率的な方法でFlutterコードを設計する方法を学びます。

ESP32 |フラッター| BLE-ダストセンサー、シンプルな空気モニターアプリ(グラフ付き)

https://www.youtube.com/watch?v=w6jo2kJanqU


ESP32を使用してFlutterアプリケーションを作成し、BLEを介してダストセンサーデータを取得する方法に関するチュートリアル。

ライブラリ&コード

バージョン2.0 |ムーア

https://moor.simonbinder.eu/v2/


Moorのバージョン2.0がリリースされました。その完全なウェブサイトをチェックして、それを最大限に活用する方法を学んでください。

ミラド・アカリエ/ form_field_validator

https://github.com/Milad-Akarie/form_field_validator

一般的な検証オプションを提供する簡単なフラッターフォームフィールドバリデーター。

timfreiheit / r_flutte

https://github.com/timfreiheit/r_flutter

すべてのフラッターアセット用に自動生成された定数。

niklas-8 / RemoteFiles

https://github.com/niklas-8/RemoteFiles


AndroidおよびiOS用のオープンソースSFTPクライアント

Timoteohss / slide_button

https://github.com/Timoteohss/slide_button

フラッターを確認するスワイプボタン

mmcc007 /シルフ

https://github.com/mmcc007/sylph

クラウド内の実際のデバイスでFlutter統合テストを実行します。

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

[swift]トルツメ機能を実装する

はじめに

Web画面とかでよくあるトルツメ機能が欲しかったのですがUIKITなどにはもちろんなく、一から手作りしたので共有します。

画面のイメージ

ezgif.com-video-to-gif.gif

コード

ViewController.swift
import UIKit

class ViewController: UIViewController {

    // トルツメ設定
    let horizontalStackView:UIStackView = UIStackView()
    let verticalStackView:UIStackView = UIStackView()

    override func viewDidLoad() {
        super.viewDidLoad()
        // 画面の大きさを取得
        let screenWidth = Int( UIScreen.main.bounds.size.width);
        let screenHeight = Int( UIScreen.main.bounds.size.height);
        print("screenWidth=\(screenWidth)")
        print("screenHeight=\(screenHeight)")

        // 横用StackView
        view.addSubview(horizontalStackView)
        horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
        // オートレイアウトオフ
        horizontalStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true

        // 上のラベル 横並びにするためにダミーのViewも作成する
        let label = UIView()
        label.backgroundColor = UIColor.clear
        label.translatesAutoresizingMaskIntoConstraints = false
        label.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth/4)).isActive = true
        label.heightAnchor.constraint(equalToConstant: 40).isActive = true

        let label2 = UIView()
        label2.backgroundColor = UIColor.clear
        label2.translatesAutoresizingMaskIntoConstraints = false
        label2.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth/4)).isActive = true
        label2.heightAnchor.constraint(equalToConstant: 40).isActive = true

        let label3 = UIView()
        label3.backgroundColor = UIColor.clear
        label3.translatesAutoresizingMaskIntoConstraints = false
        label3.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth/4)).isActive = true
        label3.heightAnchor.constraint(equalToConstant: 40).isActive = true

        let label4 = UIButton()
        label4.backgroundColor = UIColor.gray
        label4.translatesAutoresizingMaskIntoConstraints = false
        label4.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth/4)).isActive = true
        label4.heightAnchor.constraint(equalToConstant: 40).isActive = true

        // 画像
        let image:UIImage = UIImage(named:"menu")!
        let imageView = UIImageView(image:image)
        label4.addSubview(imageView)            // addしてからじゃないと制約追加できないので注意
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.centerXAnchor.constraint(equalTo: label4.centerXAnchor).isActive = true
        imageView.centerYAnchor.constraint(equalTo: label4.centerYAnchor).isActive = true
        imageView.contentMode = UIView.ContentMode.scaleAspectFit

        // 縦用StackView
        view.addSubview(verticalStackView)
        verticalStackView.translatesAutoresizingMaskIntoConstraints = false
        verticalStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
        verticalStackView.axis = NSLayoutConstraint.Axis.vertical                                    // 縦並び
        verticalStackView.alpha = 0.8

        // メインView
        let mainView = UIView()
        mainView.backgroundColor = UIColor.gray
        mainView.translatesAutoresizingMaskIntoConstraints = false
        mainView.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth)).isActive = true
        mainView.heightAnchor.constraint(equalToConstant: CGFloat(screenHeight/10)).isActive = true

        // 項目AのLabel
        let itemName = UILabel()
        itemName.textColor = .white
        itemName.text = "項目A: テスト"
        mainView.addSubview(itemName)
        itemName.translatesAutoresizingMaskIntoConstraints = false
        itemName.centerXAnchor.constraint(equalTo: mainView.centerXAnchor, constant: 15).isActive = true
        itemName.centerYAnchor.constraint(equalTo: mainView.centerYAnchor, constant: CGFloat(screenHeight/60)).isActive = true
        itemName.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth)).isActive = true
        itemName.font = UIFont.boldSystemFont(ofSize: 21)

        // 項目BのLabel
        let torishoName = UILabel()
        torishoName.textColor = .white
        torishoName.text = "項目B: test"
        mainView.addSubview(torishoName)
        torishoName.translatesAutoresizingMaskIntoConstraints = false
        torishoName.centerXAnchor.constraint(equalTo: mainView.centerXAnchor, constant: 15).isActive = true
        torishoName.centerYAnchor.constraint(equalTo: mainView.centerYAnchor, constant: -CGFloat(screenHeight/60)).isActive = true
        torishoName.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth)).isActive = true
        torishoName.font = UIFont.boldSystemFont(ofSize: 21)

        horizontalStackView.addArrangedSubview(label)
        horizontalStackView.addArrangedSubview(label2)
        horizontalStackView.addArrangedSubview(label3)
        horizontalStackView.addArrangedSubview(label4)

        verticalStackView.addArrangedSubview(horizontalStackView)
        verticalStackView.addArrangedSubview(mainView)

        // 表示時はhiddenとする
        verticalStackView.arrangedSubviews[1].isHidden = true

        // ボタンを押した時
        label4.addTarget(self, action: #selector(pushButton), for: .touchUpInside)
    }

    // ボタン押したとき
    @objc func pushButton(sender: UIButton){
        //ここにViewを隠したり表したりする処理を入れる
        verticalStackView.arrangedSubviews[1].isHidden.toggle()
        verticalStackView.layoutIfNeeded()
    }
}

StackViewを組み合わせて実現しました。
もっといい方法あるよ!などあったら教えてください。

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

[iOS]証明書がなかったらどうなるのか

なぜこの記事を書こうと思ったか

・iOSエンジニアなのに証明書の存在理由がいまいち分からなかったから
・証明書の扱いが煩雑でムカムカしていたから

どういう人に向けて書いているのか

・証明書の恩恵を知らずに扱っている人
・ざっくりと証明書の存在理由について知りたい人

二行要約

・証明書技術がないとなりすましや第三者から簡単に攻撃されてしまう
・証明書を取り巻く暗号技術を用いることによって、安心安全に二者間で情報の受け渡しが可能になる

証明書がない世界

なりすまし

Screen Shot 2019-09-11 at 2 08 24

改ざんScreen Shot 2019-09-11 at 2.03.08.png

盗聴

Screen Shot 2019-09-11 at 2 03 19

事後否認

Screen Shot 2019-09-11 at 2.04.00.png

etc

信用が成り立たず、誰もインターネットに参加しない...

証明書の仕組み

サイバートラスト社に代表される信頼された認証局が、情報通信先のサーバのサイト運営組織が実在していることを証明し、WebブラウザとWebサーバ間(サーバ同士でも可能)でSSL(Secure Socket Layer)暗号化通信を行うための電子証明書です。
引用元: SureServer

つまり信頼できる第三者機関が認めた身元証明
それがあるから安心して見ず知らずの第三者とネットワークを通してやりとりできる
iOSアプリ開発の場合、Appleが信頼できる第三者機関として証明書発行を行ってiru
Screen Shot 2019-10-08 at 0 58 54

結論

証明書があるからこそ開発者は安心してアプリ配信ができるし、ユーザーも安心してアプリを使用できることがなんとなく理解できました。
今回の記事では、技術的なことは相当端折ったのですが、なんとなく雰囲気を理解してもらえれば幸いです。

余談

暗号技術入門をマインドマップでまとめています。
証明書を構成する暗号技術についてもまとめているので、深いところまで気になる方はこちらどうそ。
暗号技術マインドマップ

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

【Swift】 「タブを閉じる」ボタンの追加(自作タブブラウザアプリ開発)

このエントリ(【Swift】 タブブラウザライクに、任意のボタンを押した際に対応するViewを最前面に表示する)の続きとして、各タブに「タブを閉じるボタン」をつけました。

開発環境

端末:MacBook Pro/MacOS 10.14.5(Mojave)
Xcode:10.2.1
Swift:5

やったこと(ポイント)

①各タブに「タブを閉じる」ボタンを追加

実装

画面イメージ

各タブの左に、赤い「×」でボタンを付けています。

3つのタブが表示された状態
Simulator Screen Shot - iPad Air 2 - 2019-10-08 at 08.32.24.png
「tabNumber2」以外を閉じた状態
Simulator Screen Shot - iPad Air 2 - 2019-10-08 at 08.32.41.png

見た目では分からないですが、「×」ボタンを押すと、対応するタブボタンとそれに紐づいているWKWebViewも消えています。
あと、消したタブの右側にタブが存在する場合、ちゃんと左に詰められるようにしています。

ソースコード

冒頭にリンクを貼ったエントリから、修正した点は大きく2つです。
①タブに「閉じる(×)」ボタンを追加表示させる
②「閉じる」ボタン押下時の処理を追加する

それ以外の部分を割愛すると、以下のようなコードで実現しています。

ViewController.swift
import UIKit
import WebKit

class ViewController: UIViewController,
    WKNavigationDelegate,
    WKUIDelegate,
    UIScrollViewDelegate {

    ... // 前回のエントリに貼ったコードを参照。

    @objc func closeTab(sender: UIButton) {
        // webViewを消す
        let viewTag = sender.tag + 9000
        let targetView = view.viewWithTag(viewTag) as! WKWebView

        targetView.removeFromSuperview()

        // タブボタンを消す
        let btnTabTag = sender.tag - 1000
        let targetBtn = view.viewWithTag(btnTabTag) as! UIButton

        targetBtn.removeFromSuperview()

        // タブボタンにひっついていた「閉じる」ボタン自体も消す
        let btnCloseTabTag = sender.tag
        let targetBtnCloseTab = view.viewWithTag(btnCloseTabTag) as! UIButton

        targetBtnCloseTab.removeFromSuperview()

        // UIScrollViewの中身を残りのタブで配置しなおす
        originX = 0
        tabScrollView.frame = CGRect.init(x: 0,
                                          y: 44,
                                          width: viewWidth,
                                          height: 50)
        tabScrollView.contentSize = CGSize.init(width: 0,
                                                height: 50)

        for i in 1...tagNumber {
            if (view.viewWithTag(i) != nil) {
                let targetButton = view.viewWithTag(i) as! UIButton
                let targetButtonWidth:CGFloat = 200
                let targetButtonHeight:CGFloat = tabScrollView.frame.height

                // タブボタンの見た目設定
                targetButton.tag = i
                targetButton.layer.cornerRadius = 3.0
                targetButton.layer.borderWidth = 1.5
                targetButton.layer.borderColor = UIColor.init(red: 87/255, green: 144/255, blue: 40/255, alpha: 1).cgColor

                targetButton.backgroundColor = .white
                targetButton.setTitleColor(UIColor.init(red: 118/255, green: 197/255, blue: 57/255, alpha: 1), for: .normal)
                targetButton.titleLabel?.font = UIFont.systemFont(ofSize: 15)
                targetButton.addTarget(self,
                                    action: #selector(showView(sender: )),
                                    for: .touchUpInside)
                targetButton.frame = CGRect(x: originX,
                                         y: 0,
                                         width: targetButtonWidth,
                                         height: targetButtonHeight)

                // タブを閉じるボタン
                let closeTagNumber = i + 1000
                let targetCloseButton = view.viewWithTag(closeTagNumber) as! UIButton
                let targetCloseButtonWidth:CGFloat = 30
                let targetCloseButtonHeight:CGFloat = 30
                targetCloseButton.tag = closeTagNumber
                targetCloseButton.addTarget(self,
                                      action: #selector(closeTab(sender:)),
                                      for: .touchUpInside)
                targetCloseButton.frame = CGRect.init(x: originX + 160,
                                                y: 10,
                                                width: targetCloseButtonWidth,
                                                height: targetCloseButtonHeight)

                // スクロールバーの中のタブボタンの表示位置をずらす制御
                originX += targetButtonWidth

                tabScrollView.addSubview(targetButton)
                tabScrollView.addSubview(targetCloseButton)
                tabScrollView.contentSize = CGSize(width: originX,
                                                   height: targetButtonHeight)
                print("originX = " + originX.description)
                view.addSubview(tabScrollView)
            }
        }        
    }

    func addTab(url: URL) {

        ... // 前回のエントリに貼ったコードを参照。

        // タブを閉じるボタン
        let btnCloseTab = UIButton()
        let btnCloseTabWidth:CGFloat = 30
        let btnCloseTabHeight:CGFloat = 30
        btnCloseTab.tag = tagNumber + 1000
        btnCloseTab.titleLabel?.textAlignment = .center
        btnCloseTab.setTitle("×", for: .normal)
        btnCloseTab.setTitleColor(.red, for: .normal)
        btnCloseTab.addTarget(self,
                              action: #selector(closeTab(sender:)),
                              for: .touchUpInside)
        btnCloseTab.frame = CGRect.init(x: originX + 160,
                                        y: 10,
                                        width: btnCloseTabWidth,
                                        height: btnCloseTabHeight)

        // スクロールバーの中のタブボタンの表示位置をずらす制御
        originX += tabButtonWidth

        // タブボタンと、閉じるボタンをUIScrollViewに追加
        tabScrollView.addSubview(tabButton)
        tabScrollView.addSubview(btnCloseTab)
        tabScrollView.contentSize = CGSize(width: originX,
                                           height: tabButtonHeight)

        view.addSubview(tabScrollView)
        view.addSubview(webView)

        ...

    }

一応「閉じる」機能を実装できました。
他の良いやり方が思いつかないですが、なんかビミョーな感じはしてます。

補足

細かいトコですが、前回のエントリではタブボタンの文字をタブ中央に配置していたんですが、「閉じる」ボタンの追加にあたり、左寄せにしました。

しかし、そのやり方がUIButton.titleLabel.textAlignment = .leftではなく、
UIButton.contentHorizontalAlignment = .leftだったので、一応補足まで。

    let tabButton = UIButton()
    let tabButtonWidth:CGFloat = 200
    let tabButtonHeight:CGFloat = tabScrollView.frame.height

    // タブボタンの見た目設定
    tabButton.tag = tagNumber
    // この設定はあまり意味がなかった。
    // tabButton.titleLabel?.textAlignment = .center
    tabButton.contentHorizontalAlignment = .left

感想など

「タブを閉じる」はできたので、次は「今開いてるタブはどれか」を示すために色を変えていたのを、タブを閉じた時にどう制御するかを考えていきます。
あと、実際動かしたいWebサービスでは、target="hoge"のように開くタブが指定されているリンクがあるので、それをきちんと制御してあげるにはどうしたらいいかが課題かなと思っています。

引き続き頑張ります。

以上です。

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