20191008のSwiftに関する記事は14件です。

<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で続きを読む

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で続きを読む

バリデーションをメソッドチェーンで書く方法

アプリ開発で、ユーザ入力が伴う画面では必ずと言っていいほど入力内容のバリデーションが必要です。ところが、もしかしてそのバリデーションのコード、読みにくすぎ?

例えばこんな要件があるとします:ユーザのメンバー ID を入力する画面があります。この ID は 10 桁以内の数字です。さてこれのバリデーションを書きましょうか:

func isIDValid(_ id: String) -> Bool {

    guard !id.isEmpty else {
        return false
    }

    guard Int(id) != nil else {
        return false
    }

    guard id.count < 10 else {
        return false
    }

    return true

}

validate("123") // true
validate("abc") // false

うーん読めなくはないですが、guard 文多いですね。今回のような単純な要件ならまあ読めなくはないですが…
あと書くのだるい…

もちろん、どうせ Bool 返してるだけなので、いっそのことこんな感じで書くのもいいですね:

func validate(_ id: String) -> Bool {

    return !id.isEmpty
        && Int(id) != nil
        && id.count <= 10

}

validate("123")
validate("abc")

これならだいぶ書きやすくなります。ただやっぱ補完効きにくいのでまだ改善の余地はあるし、何よりそもそもの話、バリデーション結果を Bool で返すのはなんかな…ですよね。

と言うわけで、これをこんな風に書きたいのです:

func validate(_ id: String) -> ValidationStatus<InvalidID> {

    IDValidator.validate(id) { $0
        .isNotEmpty()
        .isInt()
        .lessThan10Digits()
    }

}


validate("123") // .valid
validate("abc") // .invalid(.notInt)

これではメソッドチェーンが使えるのでだいぶ書きやすいし、何よりなんのバリデーションか一目瞭然ですね!

では、これをどうやって作っているのかと言うと、簡単です。まずバリデーションの型を作ります:

protocol InvalidStatus: Equatable {

}

enum ValidationStatus<Invalid: InvalidStatus> {
    case valid
    case invalid(Invalid)
}

バリデーション状態は .valid.invalid のみにし、具体的な .invalid 状態はさらに Invalid で定義してもらいます。これのメリットは、大きなスコープで見た時は有効と無効だけで分けられるのでいろんな柔軟な対応がしやすい1し、具体的になぜ無効なのかが知りたいときのその無効状態を Invalid: InvalidStatus から取り出せます。

次に、このバリデーションを実際にかける型を作ります。とは言え、どっちかと言うとこれはラッパーオブジェクトに近いですかね:

struct ValidationContainer<Target, Invalid: InvalidStatus> {

    private let target: Target
    private let invalid: Invalid?

    private func finish() -> ValidationStatus<Invalid> {

        if let invalid = invalid {
            return .invalid(invalid)

        } else {
            return .valid
        }

    }

    static func validate(_ target: Target, with validation: (Self) -> Self) -> ValidationStatus<Invalid> {

        let container = Self.init(target: target, invalid: nil)
        let result = validation(container).finish()

        return result

    }

    func guarantee(_ condition: (Target) -> Bool, otherwise invalidStatus: Invalid) -> Self {

        // If the container already has an invalid status, skip the condition check.
        guard invalid == nil else {
            return self
        }

        if condition(target) == true {
            return self

        } else {
            return ValidationContainer(target: target, invalid: invalidStatus)
        }

    }

}

このコンテナは何かと言うと、現在のバリデーション対象と無効状態を保存して、そして次のバリデーションに回すためのものです。まずは無効状態なしで作って、そして次々と guarantee を回して、もしどこか無効状態が途中であったらそれをそのまま返して、最後まで問題なかったら finish().valid を返せばいいです。

あれ? validate メソッドに finish は登場したけど guarantee 登場してなくない?と思うかもしれませんが、guarantee は実際の利用する側に公開したメソッドで、それは validatevalidation: (Self) -> Self と言うクロージャ内で利用してもらう予定だからです。

ちなみに struct にも関わらず Self が書けるのは、Swift 5.1 の新機能です2

さて、ここまでで下準備ができました。ではどう利用すればいいでしょうか?まずは InvalidStatus を作ります。今回は ID のバリデーションなので、InvalidID を作りましょう:

enum InvalidID: InvalidStatus {
    case empty
    case notInt
    case tooLong(maxCount: Int)
}

見ての通り、ID が空か、数字じゃないか、もしくは文字数が多すぎるの 3 パターンの無効判定があります。そしてその次、それぞれの判定を guarantee を使って組み込みます:

extension ValidationContainer where Target == String, Invalid == InvalidID {

    func isNotEmpty() -> Self {

        return guarantee({ !$0.isEmpty }, otherwise: .empty)

    }

    func isInt() -> Self {

        return guarantee({ Int($0) != nil }, otherwise: .notInt)

    }

    func lessThan10Digits() -> Self {

        let maxDigits = 10
        return guarantee({ $0.count <= maxDigits }, otherwise: .tooLong(maxCount: maxDigits))

    }

}

これで 文字列は空文字列じゃない文字列は数字である文字列は最大 10 文字以内 のバリデーションが書けました。最後は typealias を定義してこれらを繋げば、読みやすいバリデーションが書けますね!

typealias IDValidator = ValidationContainer<String, InvalidID>

func validate(_ id: String) -> ValidationStatus<InvalidID> {

    IDValidator.validate(id) { $0
        .isNotEmpty()
        .isInt()
        .lessThan10Digits()
    }

}


validate("123") // .valid
validate("abc") // .invalid(.notInt)

めでたしめでたし。


p.s. この書き方なんか見覚えあるぞ?と思ってくれた方もしいらっしゃったら、ありがとうございます!!そうですあの NotAutoLayout でも使ってたクロージャ×メソッドチェーンの組み方です :laughing:

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

ピンチアウトでズーム可能なUIImageViewを作成する。

ストーリーボードの準備

ViewControllerのsubViewにScrollViewを配置します。
ImageViewはコードで追加します。
スクリーンショット 2019-10-08 19.01.27.png

ソースコード

ModalImageViewController.swift
import UIKit

class ModalImageViewController: UIViewController, UIScrollViewDelegate {

    @IBOutlet weak var scrollView: UIScrollView!
    var image: UIImage? = nil
    var imageUrl: String = ""
    var imageView = UIImageView()

    override func viewDidLoad() {
        super.viewDidLoad()

        // デリゲートを設定
        self.scrollView.delegate = self
        // 最大倍率・最小倍率を設定する
        self.scrollView.maximumZoomScale = 5.0
        self.scrollView.minimumZoomScale = 1.0

        // imageViewをscrollViewいっぱいに生成
        self.imageView.frame = CGRect(x:0,y:0,width:view.frame.width,height:view.frame.height)
        scrollView.addSubview(imageView)
        self.imageView.contentMode = .scaleAspectFit

        guard let _noImage = UIImage(named:"noimage") else { return }
        self.loadImage(self.imageUrl, _noImage)

    }

    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return self.imageView
    }

    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        // ズーム終了時の処理
    }

    func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
        // ズーム開始時の処理
    }

    func zoomForScale(scale:CGFloat, center: CGPoint) -> CGRect{
        var zoomRect: CGRect = CGRect()
        zoomRect.size.height = self.scrollView.frame.size.height / scale
        zoomRect.size.width = self.scrollView.frame.size.width  / scale
        zoomRect.origin.x = center.x - zoomRect.size.width / 2.0
        zoomRect.origin.y = center.y - zoomRect.size.height / 2.0
        return zoomRect
    }


    /// 画像を非同期で取得して、ImageViewに設定します。
    /// - Parameter urlString: url文字列
    /// - Parameter defaultImage: 取得失敗したときの画像を入れてください
    private func loadImage(_ urlString: String, _ defaultImage: UIImage) -> Void {

        guard let _url = URL(string: urlString)
            else {
                self.imageView.isHidden = true
                return
        }

        DispatchQueue.global().async {
            do {
                let imageData: Data? = try Data(contentsOf: _url)
                DispatchQueue.main.async {

                    if
                        let data = imageData,
                        let image = UIImage(data: data) {

                        self.imageView.image =  image

                    } else {
                        self.imageView.image =  defaultImage
                    }

                }
            }
            catch {
                DispatchQueue.main.async {
                    self.imageView.image =  defaultImage
                }
            }
        }
    }
}

実行結果

シミュレーターで動かす場合は、「option」+ドラッグでできます。
タイトルなし.gif

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

UITextView UILabel で指定していないTimes New Romenという Font が使用される

環境

Xcode 11.1
Swift 4.2
iOS13

事象

最新の環境で、ビルドするとUITextView UILabelのテキストが、明朝体風の Times New Romen へ変わってしまう問題が発生しました。

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

Xib上は、ゴシックを指定できている。
スクリーンショット 2019-10-08 18.20.08.png

Debug view Hierarchy上では、"Times New Romen"というフォントが設定されてしまう。
スクリーンショット 2019-10-08 17.59.00.png

コード上で設定しても、上書きできず。

暫定対応

NSAttributedStringを使う場合のみ指定したフォントが読み込まれるようです。
あくまで暫定対応なので、アップデートで治る可能性もあるため、急いで直したい場合は、以下で対応できました。

let stringAttributes: [NSAttributedStringKey : Any] = [.font : UIFont.systemFont(ofSize: 14.0)]
textView.attributedText = NSAttributedString(string: "text", attributes: stringAttributes)

追記:2019/10/08 18:30 現在、iOS 13.1.2, Xcode 11.1でも直らない

参考

System font renders as Times New Roman in certain contexts

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

UITextView UILabel で Times New Romen が使用される

環境

Xcode 11.1
Swift 4.2
iOS13

事象

最新の環境で、ビルドするとUITextView UILabelのテキストが、明朝体風の Times New Romen へ変わってしまう問題が発生しました。

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

Xib上は、System を指定できている。
スクリーンショット 2019-10-09 1.45.22.png

期待値では、SFUI-Regularが使用されるはずが、
スクリーンショット 2019-10-09 1.41.54.png

Debug View Hierarchy上で確認してみると Times New Romen というフォントが設定されてしまう。
スクリーンショット 2019-10-08 17.59.00.png

暫定対応

NSAttributedStringを使う方法とFontを再設定する方法があります。
あくまで暫定対応なので、アップデートで治る可能性もあるため、急いで直したい場合は、以下で対応できました。

let stringAttributes: [NSAttributedStringKey : Any] = [.font : UIFont.systemFont(ofSize: 14.0)]
textView.attributedText = NSAttributedString(string: "text", attributes: stringAttributes)

or

// customクラスを作成する
class SystemFontTextView: UITextView {
    override var text: String! {
        didSet {
             font = UIFont.systemFont(ofSize: font?.pointSize ?? 14)
        }
    }
}

追記:2019/10/08 18:30 現在、iOS 13.1.2, Xcode 11.1でも直らない

参考

System font renders as Times New Roman in certain contexts

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

【初心者】UIの設計にはAdobeのXDが超絶便利だった件

はじめに

こんにちは、僕です。今回はアプリ設計の経験がなかった僕がとりあえずUI作成にと使用したツール「AdobeXD」が超絶便利だったので初心者の方用に参考になればと思い書きました。

使用環境

Adobe XD 22.7.12.3 (スターター)

良かった点

無料

有料版もありますが、まずはスターターで十分です。
このソフトをタダで使えるなんて良い時代に産まれました。

操作性の良いUI

何と言っても一番のメリットはここでしょう。おしゃれなアプリ作成には持ってこいです。本当に使いやすい
他にいくつかUIツールを試しましたが、UIツールなのにそもそもソフト自体のUIが悪いソフトが多すぎます。
それにくらべAdobeXDは白を基調にしたInstagramやAirbnbのなどに似たシンプルなデザインで初心者にも使いやすい用に作られています。
スクリーンショット 2019-10-08 11.59.25.png

「アプリを設計したいが、専門的な知識はない」
「でもストアでよく見るあんなダサいデザインにしたくはない」
と思う方にとっては最高のツールではないかと思います。

さらに、これはアプリ開発の話ですがデバイスプレビュー機能という実際にスマホで動かしているような画面で作品を確認する機能もついています。簡単にですがこのボタンでどこに飛ぶといった設定もできるのでおすすめです。さらに、USB接続で専用アプリを入れたiPhoneとリアルタイムで同期し、プレビューができるので、絶対に使いましょう。
スクリーンショット 2019-10-08 16.06.03.png

テンプレートが多い

AdobeXDでは世界中のクリエイターが作ったテンプレートがネットに転がっています。

https://ferret-plus.com/10394

Adobe XDで簡単にプロトタイプを作れる!おすすめの無料テンプレート15選

とても質のいいテンプレートがたくさんあります。
基本的なフレームワークができていればある程度感覚でなんとかなるものです。
特に僕と同じような学生で他の学生とデザインで差を人にとってはこのちょっとした努力で評価がかなり変わってくるでしょう!
スクリーンショット 2019-10-08 15.32.40.png
これは僕がテンプレートを参考に一時間程度で作ったフリマアプリのプロフィール画面ですが、デザイン知識のない学生でもここまでできるものです。
なお、デザイン画面はSVGでエクスポートすることでXcode等でもそのまま使えるので安心ですね。

最後に

僕と同じように「デザインの知識がないがアプリのデザインをしたい」という方には本当にぴったりのツールだと思います。
ダウンロードは下記からできます。ぜひお試しあれ。

Adobe体験版ダウンロード

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

This application’s application-identifier entitlement does not match that of the installed application. These values must match for an upgrade to be allowed

スクリーンショット 2019-10-08 13.34.50.png
下記エラーの対応
This application’s application-identifier entitlement does not match that of the installed application. These values must match for an upgrade to be allowed
1、window → devicesのタブからデバイス情報を開く
2、左のタブから実機を選択する
3、右のリストのInstalled Apps から問題のアプリを(-)にて選択削除
4、再度インストール

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

【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で続きを読む

xcodeでいきなりCould not raunch MyApp と更にprocess launch failed unspecifiedとエラー実機でのデバッグ不可!!

xcodeでいきなりCould not raunch MyApp と更にprocess launch failed unspecifiedとエラー実機でのデバッグ不可となった。
更にキーチェーンアクセスにて証明書が信頼されてないとのメッセージ。
1507569900x.jpg
https://www.apple.com/certificateauthority/
上記サイトよりAppleWWDRCA.cerファイルをダウンロードクリックでキーチェーンアクセスへ追加登録

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

[SwiftUI]リスト表示のセルにボタンが配置できない問題とその解決策

いわゆる、UITableViewCellの上にUIButtonを置くような事がしたい場合にSwiftUIでどのように記述すれば良いでしょうか。

struct ContentView: View {
    var body: some View {
        List {
            Group {
                HStack {
                    Text("Tap ME!! →")
                    Button(action: {
                        print("tap!!")
                    }, label: {
                        Text("Button")
                    })
                }
            }
        }
    }
}

このように、Listの中でButtonを書けば良さそうです。では実行してみましょう。

Screen Shot 2019-10-08 at 2.10.06.png

レイアウトは良さそうですが、ボタンが黒色ですね。
もうお気づきかと思いますが、Listの中で宣言するButtonは選択可能なCellを表示することを表現します。
同様に、複数のButtonが存在してもセルをタップした際に全てのボタンが反応するセルになってしまいます。

セル内にボタン(っぽいもの)を配置したい場合は、

struct ContentView: View {
    var body: some View {
        List {
            Group {
                HStack {
                    Text("Tap ME!! →")
                    Text("Button").onTapGesture {
                        print("tap!!")
                    }
                }
            }
        }
    }
}

このように、onTapGestureなどで強制的に反応するようにしてあげれば良いです。

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