20200301のSwiftに関する記事は12件です。

UserDefaultsをもっとシンプルに

はじめに

今回はUserDefaultsをもっと簡単に管理しやすくするためのコードを紹介していきたいと思います。是非参考にしていただけると幸いです☺️
ジェネリクスenumを利用しています。

環境

  • macOS Catalina(10.15.2)
  • xcode(11.3)
  • Swift5.1

コード

※UserDefaultsはあちこちのファイルで定義すると管理が大変になるので、このように1つのファイルで管理するといいかもです。

UserDefaults.swift
enum UserDefaultsKey: String {
    case title = "title"

    func get<T>() -> T? {
        return UserDefaults.standard.object(forKey: self.rawValue) as? T
    }

    func set<T>(value: T) {
        UserDefaults.standard.set(value, forKey: self.rawValue)
    }

    func remove() {
        UserDefaults.standard.removeObject(forKey: self.rawValue)
    }
}

利用するときはこんな感じです。

     UserDefaultsKey.title.set(value: "氷菓")
     UserDefaultsKey.title.get() ?? String()
     UserDefaultsKey.title.remove()

解説

enumを利用しenumのメソッドに登録、取得、削除の処理を定義しています。caseの部分にKeyを定義しています。UserDefaultsはどの型でも扱うことができるので、メソッドにジェネリクスを利用しています。
流れとしては、caseでキーを指定しそれに対応する値を各メソットでどう取り扱うかという感じです。

おまけ(UserDefaultsのファイルを見る!)

思ったより早く説明しきってしまったので、おまけコーナーを設けました笑。知っている方もいるかと思いますが、知らなかった方は是非参考にしてみてください。

  1. xcodeの左上にあるiPhoneマークをクリックし、「Add Additional Simulator」を選択後、下の写真を参考にファイルをダウンロードしてください。 image.png

2.ダウンロードしたファイルを「パッケージの内容を表示」を選択して開いてください。
3.次のパス(AppData/Library/Preferences/任意のファイル名.plist)にあるファイルを開くとUserDefaultsの中身を確認することができます。

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

SwiftUIで作るドラえもん【顔編】

はじめに

日曜日で暇なので, SwiftUIを使ってドラえもんを作ってみました.

この記事で完成するドラえもんは以下の画像です.
口を開けているドラえもんは難しそうなので断念しました.
かなり本物に近くないですか?笑

DoraFace

さぁ作って行きましょう.

土台づくり

まず,以下の画像のような土台となる青の円とその中にある円から作って行きましょう.
ドラえもんって意識してませんでしたが,地味に楕円なんだと感じました.
確かによくよく見たら楕円なんですよね.

土台の色のみ,Asset Catalogで色を管理しています.
(鼻も管理した方が良さそう...)

土台

以下コードです.

ContentView.swift
struct base: View {
    var body: some View {
        ZStack {
            // 土台
            Ellipse()
            .overlay(
              Ellipse()
             .stroke(Color.black,lineWidth: 2)
            )
            .frame(width: 300, height: 290)
            .foregroundColor(Color("DoraColor"))

            // 顔
            Ellipse()
                .frame(width: 270 , height: 250)
                .foregroundColor(Color.white)
                .overlay(
                  Ellipse()
                 .stroke(Color.black,lineWidth: 2)
                )
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 100 } )
        }
    }
}

次に目です.

eyeeye

ContentView.swift
struct eyes: View {
    var body: some View {
        HStack(spacing:0) {
            // 左目
            ZStack {
                Ellipse()
                    .frame(width: 60, height: 80)
                    .foregroundColor(Color.white)
                    .overlay(
                      Ellipse()
                     .stroke(Color.black,lineWidth: 2)
                    )
                ZStack {
                    Ellipse()
                        .frame(width: 20, height: 30)
                        .foregroundColor(Color.black)

                    Ellipse()
                        .frame(width:5, height: 10)
                        .foregroundColor(Color.white)
                }
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 10 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in -5 })

            }

            ZStack {
                // 右目
                Ellipse()
                    .frame(width: 60, height: 80)
                    .foregroundColor(Color.white)
                    .overlay(
                      Ellipse()
                     .stroke(Color.black,lineWidth: 2)
                    )

                ZStack {
                    Ellipse()
                        .frame(width: 20, height: 30)
                        .foregroundColor(Color.black)

                    Ellipse()
                        .frame(width:5, height: 10)
                        .foregroundColor(Color.white)
                }
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 10 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 25 } )
            }
        }
    }
}

続いては鼻ですね.

nosenose

ContentView.swift
struct nose: View {
    var body: some View {
        Group {
            // 鼻
            ZStack{
                Circle()
                    .frame(width: 50, height: 50)
                    .foregroundColor(Color.red)
                    .overlay(
                      Ellipse()
                     .stroke(Color.black,lineWidth: 2)
                    )
                Circle()
                    .frame(width: 20, height: 10)
                    .foregroundColor(Color.white)
                    .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 20})
                    .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 15})
            }
            .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 60 } )

            // 人中?
            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 110, height: 2)
                .rotationEffect(Angle(degrees: 90))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in -45 } )
        }
    }
}

ここまでくるとそれっぽくなってきました.

CatBeardCatBeard

ContentView.swift
struct CatBeard: View {
    var body: some View {
        Group {
            // 左髭
            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: 15))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 10 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 120 } )

            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: 0))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in -20 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 120 } )

            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: -15))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in -50 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 120 } )

            // 右髭
            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: -15))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 10 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in -50 } )

            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: 0))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in -20 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in -50 } )

            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: 15))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in -50 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in -50 } )
        }
    }
}

最後に口です.

mouthmouth

ContentView.swift
struct Mouth: View {
    var body: some View {
        Circle()
            .trim(from: 0.05, to: 0.45)
            .stroke(Color.black,lineWidth: 3)
            .frame(width: 200, height: 200)
    }
}

完成

無理やりですがなんとか完成しました.
DoraFace

全文
ContentView.swift
import SwiftUI

struct base: View {
    var body: some View {
        ZStack {
            // 土台
            Ellipse()
            .overlay(
              Ellipse()
             .stroke(Color.black,lineWidth: 2)
            )
            .frame(width: 300, height: 290)
            .foregroundColor(Color("DoraColor"))

            // 顔
            Ellipse()
                .frame(width: 270 , height: 250)
                .foregroundColor(Color.white)
                .overlay(
                  Ellipse()
                 .stroke(Color.black,lineWidth: 2)
                )
                .alignmentGuide(.leading, computeValue: { _ in 10 } )
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 100 } )
        }
    }
}

struct eyes: View {
    var body: some View {
        HStack(spacing:0) {
            // 左目
            ZStack {
                Ellipse()
                    .frame(width: 60, height: 80)
                    .foregroundColor(Color.white)
                    .overlay(
                      Ellipse()
                     .stroke(Color.black,lineWidth: 2)
                    )
                ZStack {
                    Ellipse()
                        .frame(width: 20, height: 30)
                        .foregroundColor(Color.black)

                    Ellipse()
                        .frame(width:5, height: 10)
                        .foregroundColor(Color.white)
                }
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 10 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in -5 })

            }

            ZStack {
                // 右目
                Ellipse()
                    .frame(width: 60, height: 80)
                    .foregroundColor(Color.white)
                    .overlay(
                      Ellipse()
                     .stroke(Color.black,lineWidth: 2)
                    )

                ZStack {
                    Ellipse()
                        .frame(width: 20, height: 30)
                        .foregroundColor(Color.black)

                    Ellipse()
                        .frame(width:5, height: 10)
                        .foregroundColor(Color.white)
                }
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 10 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 25 } )
            }
        }
    }
}

struct nose: View {
    var body: some View {
        Group {
            // 鼻
            ZStack{
                Circle()
                    .frame(width: 50, height: 50)
                    .foregroundColor(Color.red)
                    .overlay(
                      Ellipse()
                     .stroke(Color.black,lineWidth: 2)
                    )
                Circle()
                    .frame(width: 20, height: 10)
                    .foregroundColor(Color.white)
                    .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 20})
                    .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 15})
            }
            .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 60 } )
            // 人中?
            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 110, height: 2)
                .rotationEffect(Angle(degrees: 90))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in -45 } )
        }
    }
}

struct CatBeard: View {
    var body: some View {
        Group {
            // 左髭
            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: 15))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 10 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 120 } )

            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: 0))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in -20 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 120 } )

            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: -15))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in -50 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 120 } )

            // 右髭
            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: -15))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 10 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in -50 } )

            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: 0))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in -20 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in -50 } )

            Rectangle()
                .foregroundColor(Color.black)
                .frame(width: 70, height: 2)
                .rotationEffect(Angle(degrees: 15))
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in -50 } )
                .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in -50 } )
        }
    }
}

struct Mouth: View {
    var body: some View {
        Circle()
            .trim(from: 0.05, to: 0.45)
            .stroke(Color.black,lineWidth: 3)
            .frame(width: 200, height: 200)
    }
}

struct DoraFace: View {
    var body: some View {
        ZStack{
            // 土台
            base()
            // 目
            eyes()
                .alignmentGuide(VerticalAlignment.center, computeValue: { _ in 130})
            // 鼻
            nose()
            // 髭
            CatBeard()
            // 口
            Mouth()
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack{
            Text("ドラえもん")
                .font(.largeTitle)
            DoraFace()
        }

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

終わりに

あまり綺麗ではありませんが,なんとか完成させることができました.
次の暇な日曜日にでも綺麗なコードに修正していこうと思います.

ちなみに,今回のタイトルに,【顔編】とありますが,【体編】を作る予定はありません.笑

SwiftUI初心者なのでもっと良い書き方があればご教示ください.

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

【Swift】ミュータブルな要素を操作をせずに配列をDictionaryに変換する

はじめに

列挙可能なデータからDictionaryをつくりたくなることはないでしょうか。
私はありました。
私はC#を以前に学んでおり、C#にはIEnumerable.ToDictionary()という拡張メソッドがあったので、
Swiftにも同じ機能を持った関数がないか調べていました。

StackOverflowで見つかったサンプル

https://stackoverflow.com/a/38455935

let myDictionary = myArray.reduce([Int: String]()) { (dict, person) -> [Int: String] in
    var dict = dict
    dict[person.position] = person.name
    return dict
}

なるほど、reduce()の初期値に空のDictionaryのインスタンスを与えて、
配列の要素順にDictionaryインスタンスに要素を追加していけば完成する。

reduce()は畳み込んでデータの次元を落とす処理をするイメージだったので、
こんなこともできるのかと目から鱗でした。

しかし気になることが一つ。
var dict = dict
キミの存在だ。

せっかくの関数型要素を強くもつ言語なのだから
イミュータブルなデータの操作のみでできないものだろうか。 というのが以降書いている内容の動機です。
(標準ライブラリの中は除くとして)

ついでにC#にあるようなtoDictionary()拡張を用意することで、
簡単に書けるようにしてしまおうという目論みです。

そして毎度おなじみ。

もっといいものが世の中にはあるらしい。
自己流の答えを書く前に世の中で正しいとされている方法を先に紹介です。
新しいreduceによる解決策

users.reduce(into: [Int: User]()) { $0[$1.id] = $1 }

intoラベルを付けた場合、ミュータブルな状態の参照渡しで累積値が扱えるようになっているようです。
ついでに、でやりたかったことはこれで解決してしまったうえ、参照渡しなので
コピーオンライトである配列やDictionaryを扱うときのパフォーマンスも申し分ないのですが……。

とはいえ作ってみたもの

なんたって主目的はミュータブル操作の排除なので、作ったものは載せときます。

オーバーロードとか用意したら少し長くなっちゃったので
中身のコードはオンラインエディタで
(リンク消える可能性を考慮して下部にも記載しておきます)

メインの関数だけ抜き出し
    func toDictionary<TElement,TKey,TValue>(
        _ keyFunc:   (TElement) throws -> TKey,
        _ valueFunc: (TElement) throws -> TValue,
        uniquingKeysWith: (TValue, TValue) throws -> TValue
    )rethrows -> Dictionary<TKey,TValue>
    where TElement == Element {
        let mappedItems = try self
            .lazy
            .map { (key: try keyFunc($0), value: try valueFunc($0)) }   // 1.
        return try Dictionary(mappedItems,
                              uniquingKeysWith: uniquingKeysWith)       // 2.
    }

やってることは

  1. mapping関数を引数にとって、任意の配列を(key:TKey, value:TValue)のシーケンスに変換
  2. Dictionary.init(_:uniquingKeysWith:)で任意のKeyVaklueタプルのシーケンスからDictionaryインスタンスを生成。
    (キー重複が起こりえるのでその場合の処理を明示化)
使い方
let keyValues =  [1:"a",
                  2:"b",
                  3:"c",
                  4:"a",]

let keyStrSet = keyValues.keys
    .toDictionary({$0},
                  {String($0)},
                  ToDictionaryUnique.adoptLast)
print(keyStrSet) // [1: "1", 2: "2", 3: "3", 4: "4"]


let keyValueSwapLast = keyValues
    .toDictionary({$0.value},
                  {$0.key},
                  ToDictionaryUnique.adoptLast) 
print(keyValueSwapLast) // ["c": 3, "a": 4, "b": 2]

let keyValueSwapFirst = keyValues
    .toDictionary({$0.value},
                  {$0.key},
                  ToDictionaryUnique.adoptFirst) 
print(keyValueSwapFirst) // ["c": 3, "a": 1, "b": 2]

let uniqueError = try keyValues
    .toDictionary({$0.value}, 
                  {$0.key})    // Fatal error: Error raised at top level: ToDictionaryError.duplicateKeys
print(uniqueError)
どこかで見たことあるような……?

C#やってたので、C#にあったIEnumerable.ToDictionary()の使い勝手をそのまま移植した感じ。
Swiftは例外発生時の動作を漏れなく扱えるのがいいですね。

個人的な発見とか思い
  • rethrowsthrow を含む関数を引数に取った時だけtryを必要とし、含まない場合はtry記述を必要としない。
  • 最初はthrowsする関数としない関数の2種類作る必要があるんじゃないかと思っていたので、関心した。
// 引数に取った関数で値を変換する
func myMap<T,TResult>(_ target:T, _ mapFunc: (T) throws -> TResult) rethrows -> TResult {
    return try mapFunc(target)
}

enum MyError:Error{case Err}
let unwrap = { (x:Int?) -> Int in 
                guard  let y = x else { throw MyError.Err }
                return y
             }
print(    myMap(Optional(1), {x in x!}) ) // 1
print(try myMap(Optional(1), unwrap )   ) // 1
print(try myMap(Int(""),     unwrap )   ) // Fatal error: Error raised at top level: MyError.Err: 
  • 結局のところ正解例の参照渡しと、今回作ったコードでどれくらいパフォーマンス(速度、メモリ使用量)に差が出るのか。
  • swiftのメモリ使用量ってどうやってみるんだろう。
    • そもそもXCodeは使ってなくて、WindowsのWSLで動かしてるからステップ実行とか、変数内参照とかすらまだ環境が出来てない…。



往々にしてよくある悲しみ

  • 動くものを作り、文章を書いてる段になって、書いてることの正しさ確認のために資料読んでて正解を見つける。
  • やってたことは一体なんだったの感。
  • まぁ理解した内容が無駄になるわけじゃないし、目的はmutableな要素を扱わないこととしたので、意味はあるのか…?







リンク切れのときのための中身

enum ToDictionaryError:Error{
    case duplicateKeys
}

/// キー重複時の標準的な操作を提供
enum ToDictionaryUnique {
    /// シーケンスで後に登場した要素を採用
    case adoptLast
    /// シーケンスで後に登場した要素を採用
    case adoptFirst

    func getUniqueKeyFunc<TValue>()
        -> (TValue,TValue)->TValue {
        switch self {
        case .adoptFirst:
            return {(old:TValue, new:TValue) in old }
        case .adoptLast:            
            return {(old:TValue, new:TValue) in new }
        }
    }
}


extension Sequence {

    /// SequenceからDictionaryに変換する。
    /// - Parameters:
    ///   - keyFunc:   DictionaryのKeyとなる値を返す関数
    ///   - valueFunc: DictionaryのValueとなる値を返す関数
    ///   - uniquingKeysWith: キー重複時採用する値を返す関数
    func toDictionary<TElement,TKey,TValue>(
        _ keyFunc:   (TElement) throws -> TKey,
        _ valueFunc: (TElement) throws -> TValue,
        uniquingKeysWith: (TValue, TValue) throws -> TValue
    )rethrows -> Dictionary<TKey,TValue>
    where TElement == Element {
        let mappedItems = try self
            .lazy
            .map { (key: try keyFunc($0), value: try valueFunc($0)) }
        return try Dictionary(mappedItems,
                              uniquingKeysWith: uniquingKeysWith)
    }

    /// SequenceからDictionaryに変換する。
    /// - Parameters:
    ///   - keyFunc:   DictionaryのKeyとなる値を返す関数
    ///   - valueFunc: DictionaryのValueとなる値を返す関数
    ///   - uniqueCase: キー重複時の動作
    func toDictionary<TElement,TKey,TValue>(
        _ keyFunc:   (TElement) throws -> TKey,
        _ valueFunc: (TElement) throws -> TValue,
        _ uniqueCase: ToDictionaryUnique
    ) rethrows -> Dictionary<TKey,TValue>
    where TElement == Element {
        return try toDictionary(keyFunc,
                                valueFunc,
                                uniquingKeysWith: uniqueCase.getUniqueKeyFunc())
    }

    /// SequenceからDictionaryに変換する。
    /// キー重複時は例外を発生する。
    func toDictionary<TElement,TKey,TValue>(
        _ keyFunc:   (TElement) throws -> TKey,
        _ valueFunc: (TElement) throws -> TValue
    ) throws -> Dictionary<TKey,TValue>
    where TElement == Element {        
        let throwDuplicateKey = { (_:TValue, _:TValue) -> TValue in
            throw ToDictionaryError.duplicateKeys 
        } 
        return try toDictionary(keyFunc,
                                valueFunc,
                                uniquingKeysWith: throwDuplicateKey)
    }
}

let keyValues =  [1:"a",
                  2:"b",
                  3:"c",
                  4:"a",]

let keyStrSet = keyValues.keys
    .toDictionary({$0},
                  {String($0)},
                  ToDictionaryUnique.adoptLast)
print("keyStrSet")
print(keyStrSet) // [1: "1", 2: "2", 3: "3", 4: "4"]


let keyValueSwapLast = keyValues
    .toDictionary({$0.value},
                  {$0.key},
                  ToDictionaryUnique.adoptLast) 
print("keyValueSwapLast")
print(keyValueSwapLast) // ["c": 3, "a": 4, "b": 2]

let keyValueSwapFirst = keyValues
    .toDictionary({$0.value},
                  {$0.key},
                  ToDictionaryUnique.adoptFirst) 
print("keyValueSwapFirst")
print(keyValueSwapFirst) // ["c": 3, "a": 1, "b": 2]

//let uniqueError = try keyValues
//    .toDictionary({$0.value}, 
//                  {$0.key})    // Fatal error: Error raised at top level: ToDictionaryError.duplicateKeys
//print(uniqueError)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIPopoverPresentationController 背景が透明で奥のレイヤーが見えるポップオーバーにする

目的

下記のようなアプリがある場合にポップオーバーを表示するとこうなりますよね。
Simulator Screen Shot - iPhone 8 - 2020-03-01 at 16.59.32.png

このように緑色のUIViewを透かしたい場合があるのですが、
Simulator Screen Shot - iPhone 8 - 2020-03-01 at 16.59.35.png

うっすら見えますけども、透明ではないですよね。

手順

UIPopoverBackgroundViewのサブクラスを追加する。
UIPopoverPresentationControllerのプロパティ.popoverBackgroundViewClassに❶のクラスを指定する。

実装(Objective-C)

###ViewController.m
#import "ViewController.h"

@interface ViewController ()<UIPopoverPresentationControllerDelegate>

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
}

// ボタン押下後に呼び出される。
- (IBAction)tapButton:(id)sender {
    TableViewController *vc = [[TableViewController alloc]init];
    [self presentPopOverWithViewController:vc sourceView:sender];
}



- (void)presentPopOverWithViewController:(UIViewController *)viewController sourceView:(UIView *)sourceView
{
    viewController.modalPresentationStyle = UIModalPresentationPopover;
    viewController.preferredContentSize = CGSizeMake(300.0, 44.0 * 5);

    UIPopoverPresentationController *presentationController = viewController.popoverPresentationController;

    //下記記述が重要
    presentationController.popoverBackgroundViewClass = [PopoverBackgroundView class];

    presentationController.delegate = self;
    presentationController.permittedArrowDirections = UIPopoverArrowDirectionUp;
    presentationController.sourceView = sourceView;
    presentationController.sourceRect = sourceView.bounds;

    [self presentViewController:viewController animated:YES completion:NULL];
}

//iPhoneでの描画に大きく関係するのでしっかり追加しておく
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
{
    return UIModalPresentationNone;
}
@end 

PopoverBackgroundView.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface PopoverBackgroundView : UIPopoverBackgroundView

@end

NS_ASSUME_NONNULL_END

@end

PopoverBackgroundView.m

// PopoverBackgroundView.m
#import "PopoverBackgroundView.h"

@interface PopoverBackgroundView ()
{
  CGFloat _arrowOffset;
  UIPopoverArrowDirection _arrowDirection;
}
@end

@implementation PopoverBackgroundView
+ (CGFloat)arrowHeight
{
  return 0.0f;
}

+ (UIEdgeInsets)contentViewInsets
{
  return UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f);
}

- (UIPopoverArrowDirection)arrowDirection
{
  return _arrowDirection;
}

- (void)setArrowDirection:(UIPopoverArrowDirection)arrowDirection
{
  _arrowDirection = arrowDirection;
}

- (CGFloat)arrowOffset
{
  return _arrowOffset;
}

- (void)setArrowOffset:(CGFloat)arrowOffset
{
  _arrowOffset = arrowOffset;
}

- (void)layoutSubviews
{
  [super layoutSubviews];
  self.layer.shadowOpacity = 0.0f;
}
@end

変更後は下記スクリーンショットのようになります。
Simulator Screen Shot - iPhone 8 - 2020-03-01 at 17.15.22.png

実際に開発現場でどのように使われてくるか

デザイナーさんが「ポップオーバーを押下したレイヤーの上に透明に重なるように」という意図を持って、透過がある画像ファイルを作成してくださった際に、ただポップオーバー上に表示するだけではレイヤーの上に透明に重なるようには実装できません。

なのでポップオーバーの背景自体を透明色を適用する必要があります。

謝辞

下記記事を引用しております。

Xcode11、またUIPopoverPresentationControllerと明記していなかったのでこちらの記事を出させていただきました。
UIPopoverControllerの背景色を透明にする

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

Swiftをコマンドラインから実行(メモ)

環境設定

Mac OS Catalina(10.15.3)、xcodeは5.1.3が入っています。

$ sudo xcode-select -s /Applications/Xcode.app
$ sudo xcode-select --install
xcode-select: note: install requested for command line developer tools

ここでデベロッパーツールを追加でインストール。完了後はswiftを打つと通るようになりました。

$ swift
Welcome to Apple Swift version 5.1.3 (swiftlang-1100.0.282.1 clang-1100.0.33.15).
Type :help for assistance.
  1>  

コンパイル

collatz.swift
var n = 100
repeat {
    print("\(n)")
    if n % 2 == 0{
    n /= 2
    }else{
    n = n * 3 + 1
    }
}while n > 1
print(n)

これを保存しておきます。

$ swiftc collatz.swift

このコマンドでコンパイルできます。

screenshot.png

すると実行ファイルが作成されます。

これを実行します。

$ ./collatz

実行結果
100
50
25
76
38
19
58
29
88
44
22
11
34
17
52
26
13
40
20
10
5
16
8
4
2
1


コンパイルせずに解釈実行

上のファイルを解釈実行させます。

swift collatz.swift

参考文献

『詳解Swift 第5版』

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

Google Maps iOS Utils で Marker をまとめて表示する

Google Maps SDK を使用していてマップ上にマーカーを立てることはよくあると思います
ただ、マップ上にマーカーを建てすぎると、広域表示にした際にマーカーが密集しちゃって見辛くなってしまうってこともよくあると思います

そんな時マーカーを1つにまとめたいなーと思ったので、その実装方法を残しておきたいと追います

GrWyuKwR.gif

開発環境

Xcode:11.1(11A1027)
Swift 5
iOS:13.1
CocoaPods: 1.8.0

実装

ライブラリのインストール

Google Maps iOS Utils をインストールするために CocoaPods を使用します
以下のように Podfile を編集し、 pod install を実行してください

target 'YOUR_APPLICATION_TARGET_NAME_HERE' do
  use_frameworks!

  pod 'GoogleMaps'
  pod 'Google-Maps-iOS-Utils'
end

こちらのセットアップガイドにも記載されていますが、
CocoaPods のバージョンが 1.8.1 以上はサポート対象外のようです

1.6.1 が推奨されているようですが、Xcode11 では 1.6.1 を使用した場合にビルドが通らなかったため、今回は 1.8.0 を使用しています

CocoaPods のバージョンを下げるにはこちらを参考にしてください

マーカーをまとめて表示する用の class を作成

普段は GMSMarker class を使用してマーカーを表示していますが、
マーカーをまとめて表示するには GMUClusterItem protocol に準拠した class を作成し、その class を使用してマーカーを表示する必要があります

import GoogleMapsUtils

class POIItem: NSObject, GMUClusterItem {

    var position: CLLocationCoordinate2D

    init(position: CLLocationCoordinate2D) {
        self.position = position
    }
}

Map 上にマーカーを配置

先ほど作成した独自 class POIItem を使用してマーカーを Google Map 上に表示していきます

import UIKit
import GoogleMaps
import GoogleMapsUtils

class ViewController: UIViewController {

  // デフォルトの位置情報(仮で東京駅付近にしています)
    let defaultPositionLat = 35.681223
    let defaultPositionLng = 139.767059
    private var mapView: GMSMapView!
    /// Map 上に表示するマーカーを管理するためのプロパティ
    private var clusterManager: GMUClusterManager!

    override func viewDidLoad() {
        super.viewDidLoad()

        // GoogleMapの初期位置
        let camera = GMSCameraPosition.camera(withLatitude: defaultPositionLat, longitude: defaultPositionLng, zoom: 17.0)
        mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
        view = mapView

        let iconGenerator = GMUDefaultClusterIconGenerator()
        let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
        let renderer = GMUDefaultClusterRenderer(mapView: mapView, clusterIconGenerator: iconGenerator)
        clusterManager = GMUClusterManager(map: mapView, algorithm: algorithm, renderer: renderer)

        // マーカーをランダムに生成して Map 上に表示
        generateClusterItems()
    }

    private func generateClusterItems() {
        let extent = 0.01
        for _ in 1...100 {
            let lat = defaultPositionLat + extent * randomScale()
            let lng = defaultPositionLng + extent * randomScale()
            let item = POIItem(position: CLLocationCoordinate2DMake(lat, lng))
            clusterManager.add(item)
        }
        // Map にマーカーを描画
        clusterManager.cluster()
    }

    /// ランダムな位置にマーカーを表示するための乱数を生成
    private func randomScale() -> Double {
        return Double(arc4random()) / Double(UINT32_MAX) * 2.0 - 1.0
    }
}

上記のコードを実行すると、最初に載せた gif のような挙動になるかと思います

Delegate とか

マーカーをタップした際のアクションを設定したい場合、以下のように GMUClusterManagerDelegateGMSMapViewDelegate を設定してあげるとタップ時のアクションが設定可能な脳です

class ViewController: UIViewController, GMUClusterManagerDelegate, GMSMapViewDelegate {

    private var mapView: GMSMapView!
    private var clusterManager: GMUClusterManager!

    override func viewDidLoad() {
        super.viewDidLoad()

        // ... Rest of code omitted for easy reading.

        // Register self to listen to both GMUClusterManagerDelegate and
        // GMSMapViewDelegate events.
        clusterManager.setDelegate(self, mapDelegate: self)
    }

    // MARK: - GMUClusterManagerDelegate
    func clusterManager(clusterManager: GMUClusterManager, didTapCluster cluster: GMUCluster) {
        let newCamera = GMSCameraPosition.cameraWithTarget(cluster.position,
      zoom: mapView.camera.zoom + 1)
        let update = GMSCameraUpdate.setCamera(newCamera)
        mapView.moveCamera(update)
    }

    // MARK: - GMUMapViewDelegate
    func mapView(mapView: GMSMapView, didTapMarker marker: GMSMarker) -> Bool {
        if let poiItem = marker.userData as? POIItem {
            NSLog("Did tap marker for cluster item \(poiItem.name)")
        } else {
            NSLog("Did tap a normal marker")
        }
        return false
    }
}

終わりに

Google Maps iOS Utils というライブラリはマーカーをまとめるだけでなく、
マーカーの画像をカスタマイズしたり KML や GeoJSON をレンダリングできたりと他にも色々な機能が使用できるようなので、機会があれば触ってみようかと思います

参考

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

Google Maps iOS Utils でマーカーをまとめて表示する

Google Maps SDK を使用していてマップ上にマーカーを立てることはよくあると思います
ただ、マップ上にマーカーをたてすぎると、広域表示にした際にマーカーが密集しちゃって見辛くなってしまうってこともよくあると思います

そんな時マーカーを1つにまとめたいなーと思ったので、その実装方法を残しておきたいと追います

GrWyuKwR.gif

開発環境

Xcode:11.1(11A1027)
Swift 5
iOS:13.1
CocoaPods: 1.8.0

実装

ライブラリのインストール

Google Maps iOS Utils をインストールするために CocoaPods を使用します
以下のように Podfile を編集し、 pod install を実行してください

target 'YOUR_APPLICATION_TARGET_NAME_HERE' do
  use_frameworks!

  pod 'GoogleMaps'
  pod 'Google-Maps-iOS-Utils'
end

こちらのセットアップガイドにも記載されていますが、
CocoaPods のバージョンが 1.8.1 以上はサポート対象外のようです

1.6.1 が推奨されているようですが、Xcode11 では 1.6.1 を使用した場合にビルドが通らなかったため、今回は 1.8.0 を使用しています

CocoaPods のバージョンを下げるにはこちらを参考にしてください

マーカーをまとめて表示する用の class を作成

普段は GMSMarker class を使用してマーカーを表示していますが、
マーカーをまとめて表示するには GMUClusterItem protocol に準拠した class を作成し、その class を使用してマーカーを表示する必要があります

import GoogleMapsUtils

class POIItem: NSObject, GMUClusterItem {

    var position: CLLocationCoordinate2D

    init(position: CLLocationCoordinate2D) {
        self.position = position
    }
}

Map 上にマーカーを配置

先ほど作成した独自 class POIItem を使用してマーカーを Google Map 上に表示していきます

import UIKit
import GoogleMaps
import GoogleMapsUtils

class ViewController: UIViewController {

  // デフォルトの位置情報(仮で東京駅付近にしています)
    let defaultPositionLat = 35.681223
    let defaultPositionLng = 139.767059
    private var mapView: GMSMapView!
    /// Map 上に表示するマーカーを管理するためのプロパティ
    private var clusterManager: GMUClusterManager!

    override func viewDidLoad() {
        super.viewDidLoad()

        // GoogleMapの初期位置
        let camera = GMSCameraPosition.camera(withLatitude: defaultPositionLat, longitude: defaultPositionLng, zoom: 17.0)
        mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
        view = mapView

        let iconGenerator = GMUDefaultClusterIconGenerator()
        let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
        let renderer = GMUDefaultClusterRenderer(mapView: mapView, clusterIconGenerator: iconGenerator)
        clusterManager = GMUClusterManager(map: mapView, algorithm: algorithm, renderer: renderer)

        // マーカーをランダムに生成して Map 上に表示
        generateClusterItems()
    }

    private func generateClusterItems() {
        let extent = 0.01
        for _ in 1...100 {
            let lat = defaultPositionLat + extent * randomScale()
            let lng = defaultPositionLng + extent * randomScale()
            let item = POIItem(position: CLLocationCoordinate2DMake(lat, lng))
            clusterManager.add(item)
        }
        // Map にマーカーを描画
        clusterManager.cluster()
    }

    /// ランダムな位置にマーカーを表示するための乱数を生成
    private func randomScale() -> Double {
        return Double(arc4random()) / Double(UINT32_MAX) * 2.0 - 1.0
    }
}

上記のコードを実行すると、最初に載せた gif のような挙動になるかと思います

Delegate とか

マーカーをタップした際のアクションを設定したい場合、以下のように GMUClusterManagerDelegateGMSMapViewDelegate を設定してあげるとタップ時のアクションが設定可能なようです

class ViewController: UIViewController, GMUClusterManagerDelegate, GMSMapViewDelegate {

    private var mapView: GMSMapView!
    private var clusterManager: GMUClusterManager!

    override func viewDidLoad() {
        super.viewDidLoad()

        // ... Rest of code omitted for easy reading.

        // Register self to listen to both GMUClusterManagerDelegate and
        // GMSMapViewDelegate events.
        clusterManager.setDelegate(self, mapDelegate: self)
    }

    // MARK: - GMUClusterManagerDelegate
    func clusterManager(clusterManager: GMUClusterManager, didTapCluster cluster: GMUCluster) {
        let newCamera = GMSCameraPosition.cameraWithTarget(cluster.position,
      zoom: mapView.camera.zoom + 1)
        let update = GMSCameraUpdate.setCamera(newCamera)
        mapView.moveCamera(update)
    }

    // MARK: - GMUMapViewDelegate
    func mapView(mapView: GMSMapView, didTapMarker marker: GMSMarker) -> Bool {
        if let poiItem = marker.userData as? POIItem {
            NSLog("Did tap marker for cluster item \(poiItem.name)")
        } else {
            NSLog("Did tap a normal marker")
        }
        return false
    }
}

終わりに

Google Maps iOS Utils というライブラリはマーカーをまとめるだけでなく、
マーカーの画像をカスタマイズしたり KML や GeoJSON をレンダリングできたりと他にも色々な機能が使用できるようなので、機会があれば触ってみようかと思います

参考

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

【Xcode11対応】Prefix.pchの設定のやり方

記事の背景

いつもプロジェクトを新規作成する際はいつも指示を出して追加していましたが、

改めて調べた所、Xcode6から自動追加されないようになり、その頃とやり方が大きく異なるので最新のやり方についてまとめておきます。

手順

❶プロジェクトファイルの直下に「PrefixHeader.pch」を追加する。
❷対象TARGETのBulid SettingsPrefixHeaderで上記ファイルのパスを指定する。

プロジェクトファイルの直下に「PrefixHeader.pch」を追加する。

Command+nで新規ファイルを追加して「Other」>「PCH FILE」を選択。
スクリーンショット 2020-03-01 14.34.19.png
名前は変更しても構いませんが本記事はデフォルトネームを取り扱いますのでご了承ください。

スクリーンショット 2020-03-01 14.34.44.png
上記のようにプロジェクトファイル直下になれば問題ございません。

(後にパスを指定する都合上こちらの方が都合が良かった)

対象TARGETのBulid SettingsPrefixHeaderで上記ファイルのパスを指定する。

スクリーンショット 2020-03-01 14.37.35.png

上記「Prefix Header」を押下して$(SRCROOT)/$(PROJECT)/PrefixHeader.pchと設定する。
スクリーンショット 2020-03-01 14.38.57.png

これで「PrefixHeader.pch」を適用できておりますので、

よく使うクラスなどは#import "hogeViewController.h"など書いてください。

PrefixHeader.pch

#ifndef PrefixHeader_pch
#define PrefixHeader_pch

// importしたいクラスを記載する。
#import "HogeViewController.h"
// Include any system framework and library headers here that should be included in all compilation units.
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file.

#endif /* PrefixHeader_pch */

また以前記事書きましたが下記の色の定義の宣言はかなり便利なのでオススメです。活用してみてください。

#define RGB(r, g, b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1]
#define RGBA(r, g, b, a)    [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:(a)]

余談:$(PROJECT)など確認する方法

これは完全な脱線です。ただ他にはどんなコマンドがあるのだ?など疑問ある人だけ進めてください。

❶TAGERT > BuildPhases > 「+」 > New Run Script Phase
❷RunScriptにenv > env.txtを登録してBuild(Command+R)する
❸プロジェクトフォルダに env.txt というファイルが生成されるので中身を確認する。

スクリーンショット 2020-03-01 14.45.37.png
スクリーンショット 2020-03-01 14.46.07.png

こんな感じで実行したプロジェクト内に格納されているかと。
これをテキストエディタにて開いて、希望のコマンド探してみてください。
スクリーンショット 2020-03-01 14.46.22.png

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

【Swift】 配列要素の合計を求める拡張

はじめに

数値計算を総称的に扱うためのNumeric Protocolみたいなのないの?
→Swift5からあるらしいで。
それを知らなくて自分で考えてた話です。


A

Swift5の標準に基づく実装と自前実装

Swift5の標準に基づく実装

まずは正しい(と思っている)答え

standard
extension Sequence where Element:AdditiveArithmetic  {
    func sum() -> Element {
        return reduce(.zero, +)
    }
}

AdditiveArithmeticとは…
基本の数値型(Int、Doubleなど)で実装済みのプロトコル。(Swift5から)
zero(初期値)と加減算オペレータが定義されている

standard
protocol AdditiveArithmetic {
    static var zero: Self
    static func + (Self, Self) -> Self
    static func += (Self, Self)
    static func - (Self, Self) -> Self
    static func -= (Self, Self)
}


標準実装があるのを知らなくて自分で定義したプロトコルと実装。

要らないんですが考えの過程を残すために載せておきます。

my_implementation
protocol Summable{
    init()
    static func +(lhs: Self, rhs: Self) -> Self
}

extension Int:Summable{}
extension Int64:Summable{}
extension String:Summable{}
// any more 

extension Sequence where Element:Summable  {
    func sum<T:Summable>() -> Element where Element==T {
        return reduce(T.init(), +)
    }
}

// use eg
print( [1,2,3].sum() )        // …6
print( ["1","2","3"].sum() )  // …"123"
実装にあたり悩んだところ。
  • protocolはclassとは異なる要領で総称型に対する処理を実現している。
    • class … Class<T>の要領で総称型(ジェネリクス)を定義して利用 (struct、関数も同様)
    • protocol … プロパティで総称型として扱いたい型情報を管理(associatedtype)
    • 扱いが異なる理由 … なぜSwiftのプロトコルはジェネリクスをサポートしないのか
    • extension ExtProtocol where AssoociatedType:MyProtoclwhereは拡張対象を絞るだけであり、AssociateTypeを決定しているわけではない。protocolに対して汎用処理を実現したい場合、各関数に対してジェネリクス<T>T == AssociateTypeだとして定義しなければ定まらない。」ことが最初理解できなかった。
  • 合計値計算の基準となる初期値(数値であれば、各型のゼロ)をどのように定義するか。
    • reduceの第一引数に型を指定た初期値を与える必要がある。(異なる型同士は加算できないため)
    • 利用時の記述を楽にしたい。※
    • init()で初期値が取得でき、楽ができた。
※利用時にこんな風に{}内の記述したくない
extension Double:Summable{  
     static var zero:Self { return Double(0)}
}
//protocol Summable{
//    static func +(lhs: Self, rhs: Self) -> Self
//    static var zero:Self{get}
//}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】同期的にHTTP通信する

ググったところ古い情報ばかり見つかったので、Swift5でも大丈夫だったよという記事

全2回のHTTPリクエストを行う際に、
2回目のリクエストは、1回目の通信結果を使用してパラメータを作るとかそういう用途で

環境

Xcode 11.3.1
Swift 5

方法

1.DispatchSemaphoreをvalue0で生成する
2.HTTP通信した直後、セマフォをwaitする
3.HTTP通信の完了イベント内でsignal()を呼び出す

ソースコード

    /// セマフォ
    var semaphore : DispatchSemaphore!

    func syncHttpRequest()
    {
        semaphore = DispatchSemaphore(value: 0)

        // Httpリクエストの生成
        var request = URLRequest(url: URL(string: "http://httpbin.org/get")!)
        request.httpMethod = "GET"

        // HTTPリクエスト実行
        URLSession.shared.dataTask(with: request, completionHandler: requestCompleteHandler).resume()

        // requestCompleteHandler内でsemaphore.signal()が呼び出されるまで待機する
        semaphore.wait()
        print("request completed")
    }

    func requestCompleteHandler(data:Data?,res:URLResponse?,err:Error?)
    {
        print("response recieve")
        semaphore.signal()
    }

実行結果

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

【Swift5】Realmでデータを管理する

はじめに

 メモアプリのタイトルのみをUserDefaultsで管理していたのですが、タグや日付などの情報も管理したくなり、UserDefaultsで保持すると複雑になりそうだったので、Realmへの移行を試みました!
 データの挿入まではわりとスムーズにできたのですが、autoIncrementや更新系の設定で、苦戦したので、その辺りを中心にまとめます!

環境

  • xcode 11.3.1
  • swift 5.1.3

Realmにデータを挿入する

 Realmにデータを挿入するには、Objectのモデルを作り、addという関数を使って保存してあげれば、データが挿入されるようです。

Memo.Swift
import Foundation
import RealmSwift

class Memo: Object {
    @objc dynamic var title : String = ""
    @objc dynamic var content : String = ""
    @objc dynamic var tags : String = ""
    @objc dynamic var createdDate : Date = NSDate() as Date
    @objc dynamic var updatedDate : Date = NSDate() as Date

}

モデルは↑のように作りました!
データを挿入する際のコードはこんな感じです!

MemoTableViewController.swift(一部抜粋)
let memoModel = Memo()
let realm = try! Realm()

// オブジェクトに値をセットする
memoModel.title = memo
memoModel.content = memo
memoModel.tags = "test1"

// DBに書き込む
try! realm.write {
    realm.add(memoModel)
}

データを挿入するところまではこちらの記事を参考にしていただければ、わかりやすいと思います!
私もこちらを参考に実装しました!
[Realm][Swift4対応 完全保存版] 3.モデルオブジェクトの作成と書き込み

Realm内のデータを確認する

Realmのデータを確認する手順は次のようになります。

  1. Realm Browserをインストールする
  2. アプリを起動し、RealmのデータベースのPathを調べる
  3. ターミナルからopenコマンドで2のPathを開く

私はこの手順で動作確認できました!
スクリーンショット 2020-02-25 0.24.54.png

動作確認する方法はこちらの記事が参考になると思います!
【Swift】Realm BrowserでRealm Mobile Databaseの中身を確認する

idを自動連番(Auto Increment)で入れる

RealmはデフォルトでAuto Incrementするような機能がなさそう?だったので私はRObjectというクラスを作り、
そこにAuto Incrementを実装して、モデルで継承するように実装しました。

Memo.Swift
import Foundation
import RealmSwift

class Memo: RObject { // RObjectObjectに変更
    @objc dynamic var title : String = ""
    @objc dynamic var content : String = ""
    @objc dynamic var tags : String = ""
    @objc dynamic var createdDate : Date = NSDate() as Date
    @objc dynamic var updatedDate : Date = NSDate() as Date

}
RObject.swift
import Foundation
import Realm
import RealmSwift

class RObject: Object {
    // ID
    @objc dynamic var id = 0

    // データを保存。
    func save() {
        let realm = try! Realm()
        if realm.isInWriteTransaction {
            if self.id == 0 { self.id = self.createNewId() }
            realm.add(self, update: true)
        } else {
            try! realm.write {
                if self.id == 0 { self.id = self.createNewId() }
                realm.add(self, update: true)
            }
        }
    }

    // 新しいIDを採番します。
    private func createNewId() -> Int {
        let realm = try! Realm()
        return (realm.objects(type(of: self).self).sorted(byKeyPath: "id", ascending: false).first?.id ?? 0) + 1
    }

    // プライマリーキーの設定
    override static func primaryKey() -> String? {
        return "id"
    }
}
MemoTableViewController.swift(一部抜粋)
        if let selectedIndexPath = self.tableView.indexPathForSelectedRow{
            self.memos[selectedIndexPath.row] = memo

            // realm
            let realm = try! Realm()
            let memoModel = realm.objects(Memo.self).filter("id == " + String(sourceVC.memoId!)).first ?? Memo()
            try! realm.write {
                memoModel.title = memo
                memoModel.save() // realm.addから↑実装した.saveに変更
            }
        }else{
            self.memos.append(memo)
            // realm
            let memoModel = Memo()
            let realm = try! Realm()
            try! realm.write {
                memoModel.title = memo
                memoModel.content = memo
                memoModel.tags = "test1"
                memoModel.save()
            }

参考にした記事

データの更新をする

 ここまではわりとスムーズに行ったのですが、データ更新あたりから妻付き始めました。。
Realm().add(_:update:)を使ってデータを更新するという情報が多かったのですが、なぜかエラーに。。
色々調べた結果、次のように実装変えたらコンパイル通ったので参考にしてください。
おそらく、RealmSwiftの最新版だと
Realmのドキュメント

realm.add(self, update: true)
// ↓
realm.add(self, update: .modified)

元のテーブル(モデル)を削除する

 実装自体のエラーはなくなって自動連番の機能や主キーのIDが入れられるようになりました。
が、今度は起動時にIDがテーブルに存在しないというエラーが出ました。。
 調べたところ、テーブルの形式を変更したので、元々入っていたデータにIDがなく、エラーのようでした。。
対処方法は次の2つでした。

  • 既存のテーブル(Object)の削除
  • マイグレーション

まだ、不要なデータしかなかったのとマイグレーションが最初うまくできなかったので今回は、一旦既存テーブル削除を実施!

AppDelegate.swift
// 既存テーブルの削除
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let realmURL = Realm.Configuration.defaultConfiguration.fileURL!
        let realmURLs = [
            realmURL,
            realmURL.appendingPathExtension("lock"),
            realmURL.appendingPathExtension("note"),
            realmURL.appendingPathExtension("management")
        ]
        for URL in realmURLs {
            do {
                try FileManager.default.removeItem(at: URL)
            } catch {
                // handle error
            }
        }
  return true
}

 このコードを実装した状態で、アプリケーション起動するとテーブルが削除されるので、新しいモデルが挿入できるようになると思います!
 マイグレーションの方法に関しては、勉強して追記しようと思います?

参考にした記事

まとめ

 今回はRealmでデータ管理がするのに必要な実装をしてみました。updateのあたりとかは特にBool型を引数に実装をしている記事が多かったので、要注意だと思います!
 また、swift周りは技術の進化が早く、どんどんバージョンが入れ替わり、参考にした記事のコードが動かないということがザラにあったので、書かれた時期と環境のバージョンが大事だなと思いました!
あと余談ですがswiftの独特のコーディングにも少し慣れてきた気がしました(segueなど)!笑

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

[swift5]iOSアプリでローカル通知を実装

ローカル通知を実装する方法を紹介します!

動作環境

対象 バージョン
iOS 13.3.1
macOS Catalina 10.15.3
Xcode 11.3.1
Swift 5.1.3

実装の方法

ソースコード

AppDelegate.swift

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        // 通知許可の取得
        UNUserNotificationCenter.current().requestAuthorization(
        options: [.alert, .sound, .badge]){
            (granted, _) in
            if granted{
                UNUserNotificationCenter.current().delegate = self
            }
        }
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }

}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
    {
        // アプリ起動時も通知を行う
        completionHandler([ .badge, .sound, .alert ])
    }
}

ViewController.swift

import UIKit

class ViewController: UIViewController {

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

    /// ローカル通知ボタンを押下した際の処理
    /// - Parameter sender: ローカル通知ボタン
    @IBAction func localPush(_ sender: Any) {
        let content = UNMutableNotificationContent()
        content.title = "お知らせ"
        content.body = "ボタンを押しました。"
        content.sound = UNNotificationSound.default

        // 直ぐに通知を表示
        let request = UNNotificationRequest(identifier: "immediately", content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む