20210129のSwiftに関する記事は7件です。

SwiftUIでモーダル表示をしてみよう!

今回は、SwiftUIでハーフモーダルビューの作り方に関して紹介したいと思います。

動作

RocketSim Recording - iPhone 12 mini - 2021-01-29 23.09.37.gif

「モーダルを表示」ボタンを押下→モーダル表示
「閉じる」or再度「モーダルを表示」を押下→モーダル非表示
の切り替えを行なっています。

プログラム

ContentView.swift
import SwiftUI

struct ContentView: View {
    @State var screenHeight: CGFloat = 0.0
    @State private var showHalfModal = false

    var body: some View {
        GeometryReader(content: { geometry in
            ZStack{
                Button(action: {
                    self.showHalfModal.toggle()
                }, label: {
                    Text("モーダルを表示")
                        .font(.largeTitle)
                })
                HalfModal(closedButton: $showHalfModal,
                          sizeY: screenHeight,
                          objectHeight: 400.0)
            }
            .onAppear(perform: {
                screenHeight = geometry.size.height  // スクリーンサイズ(縦幅)を取得
            })
        })
        .ignoresSafeArea(.all) // 上下のセーフエリアを無効化
    }
}

struct HalfModal: View {
    @Binding var closedButton: Bool
    let sizeY: CGFloat
    let objectHeight: CGFloat

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 50.0)
                .foregroundColor(.red)
            VStack {
                Spacer()
                Button(action: {
                    closedButton.toggle()
                }, label: {
                    Text("閉じる")
                        .foregroundColor(.white)
                        .font(.largeTitle)
                })
                Spacer()
            }
        }
        .frame(height: objectHeight)
        .offset(y: closedButton ? sizeY-objectHeight :  sizeY)
    }
}

プログラムの説明

GeometryReaderで画面サイズ取得

.ignoresSafeArea(.all)でContentViewのSafeAreaを無効化、geometry.size.heightで画面全体の縦幅を取得し、screenHeight変数に代入します。

ZStackで同時に表示

ContentViewHalfModalをZStackで囲むことによって、一つの画面内に2つのViewコンポーネントを表示します。そして、HalfModal側でoffset()(表示位置の移動)を宣言することで、画面下部にハーフモーダルが表示されるようになります。

offsetで表示位置を変更

Swiftは真偽値で処理を分岐できる
[Bool] ? [trueの処理]:[falseの処理]

といった処理を書くことができます。

ですから、

  • true:y座標を「全体の縦幅」の位置に表示
  • false:y座標を「全体の縦幅」-「モーダルの縦幅」の位置に表示

と指定してあげればアクションを起こす(=Bool値を変更する)と他のViewが表示される処理を実装することができます。

ソースコード

https://github.com/r0227n/SwiftUI/tree/master/HalfModalView

GitHub上で公開していますので、参考程度にどうぞ。

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

【Swift】構造体まとめ

はじめに

この記事では、Swiftで多用する構造体についてまとめました。

参考

この記事は、以下の情報を参考にして執筆しました。

構造体とは

構造体とは、値型の一種であり、ストアドプロパティの組み合わせによって一つの値を表す。

定義方法

構造体の定義にはstructキーワードを使用する。

struct 構造体名 {
構造体の定義
}

プロパティやメソッド、イニシャライザなどの型を構成する要素は構造体で全て利用可能で、{}内に定義できる。

ストアドプロパティの変更による値の変更

構造体のストアドプロパティを変更することは、構造体を別の値に変更することであり、その時は構造体が入っている変数や定数への再代入が必要となる。
ただし、定数に代入されている場合ストアドプロパティは変更できない。

メソッド内のストアドプロパティの変更

構造体のストアドプロパティの変更は再代入を必要とするため、ストアドプロパティの変更を含むメソッドにはmutatingキーワードが必要。
ストアドプロパティの変更を含んでいるにもかかわらずmutatingキーワードがついていない場合、コンパイルエラーとなる。

メンバーワイズイニシャライザ

型のインスタンスは初期化後にすべてのプロパティが初期化されている必要がある。
独自にイニシャライザを定義して初期化の処理を行うこともできるが、構造体では自動的に定義されるメンバーワイズイニシャライザというイニシャライザを利用できる。
メンバーワイズイニシャライザは、型が持っている各ストアドプロパティと同名の引数をとるイニシャライザ。
また、ストアドプロパティが初期化式とともに定義されている場合、そのプロパティに対応するメンバーワイズイニシャライザの引数はデフォルト引数を持ち、呼び出し時の引数の指定を省略できる。

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

コードによるimageの切り替え

flagを立ててimageの切り替えを制御

ViewController.swift
var flg = false


@IBAction func button(_ sender: Any) {      
  if flg {
            flg = false
            //画像なし
            self.button.setImage(nil, for: .normal)
        } else {
            button.setImage(pause, for: UIControl.State())
            flg = true
        }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

addChildでUIViewControllerに親子関係を持たせる

addChild(childController: UIViewController)を使うとUIViewControllerの階下にUIViewControllerを持たせることができます。UI部品を配置するためのaddSubView()と違いaddChild()はUIViewControllerそのものを重ねることができます。

スクリーンショット 2021-01-29 14.01.45.jpg

ソースコード

親となるViewControllerではaddChild()だけでなくaddSubview()も呼ばないと階層関係になっていることを確認できなかった。

ParentViewController
class ParentViewController: UIViewController {

    override func viewDidLoad() {
        let childVC = ChildViewController()
        super.viewDidLoad()
        view.backgroundColor = .red
        view.addSubview(childVC.view)
        addChild(childVC)
        childVC.didMove(toParent: self)
    }
}
class ChildViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .blue
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

投稿記事の Markdown 中のコードをファイルとして取り出す (Swift版)

投稿記事の Markdown 中のコードをファイルとして取り出す」の Swift 版です。

記事に、全体は小さいけど、多数のファイルで構成されたソースコードを入れるとき

  • 記事にするときの手間
  • 利用するときの手間

を軽減する目的で作りました。Swift の Command Line Tool です。

ファイルから Markdown へ
f2m.swift
import Foundation
import AppKit

// ############

var suffix_map = Dictionary<String, String>()
let code_suffix_table = [
    ["applescript", "scpt"],
    ["ada", "ada"],
    ["awk", "awk"],
    ["bash", "bash"],
    ["bat", "bat"],
    ["bpf", "bpf"],
    ["cl", "cl"],
    ["css", "css"],
    ["csharp", "cs"],
    ["cuda", "cu"],
    ["c++", "c", " cc", " cpp", " c++", " hpp", " h++"],
    ["d", "d"],
    ["diff", "diff"],
    ["ecl", "ecl"],
    ["elisp", "el"],
    ["elm", "elm"],
    ["email", "eml"],
    ["eps", "eps"],
    ["erl", "erl"],
    ["erb", "erb"],
    ["fea", "fea"],
    ["ff", "ff"],
    ["fortran", "f", " f90", " f95", " f03", " f15"],
    ["fsharp", "fs"],
    ["gd", "gd"],
    ["glsl", "glsl", " glslv", " frag", " vert"],
    ["go", "go"],
    ["groovy", "groovy", " gvy", " gy", " gsh"],
    ["haml", "haml"],
    ["hack", "hack"],
    ["hcl", "hcl"],
    ["hlsl", "hlsl"],
    ["hql", "hql"],
    ["hs", "hs"],
    ["html", "html"],
    ["hx", "hx"],
    ["hy", "hy"],
    ["idlang", "idl"],
    ["java", "java"],
    ["js", "js"],
    ["json", "json'"],
    ["ksh", "ksh"],
    ["latex", "latex"],
    ["lhs", "lhs"],
    ["llvm", "ll"],
    ["lua", "lua"],
    ["make", "mak", " mk"],
    ["mathematica", " nb"],
    ["matlab", "mat"],
    ["md", "md"],
    ["mf", "mf"],
    ["minizinc", "mzn"],
    ["mkd", "mkd"],
    ["moon", "moon"],
    ["nim", "nim"],
    ["objcpp", "mm"],
    ["ocaml", " mli"],
    ["pascal", "p", " pas", " pp"],
    ["patch", "patch"],
    ["php", "php"],
    ["pl", "pl"],
    ["plist", "plist"],
    ["ps", "ps"],
    ["py", "py"],
    ["pyx", "pyx"],
    ["rb", "rb"],
    ["rs", "rs"],
    ["saas", "saas"],
    ["sas", "sas"],
    ["scala", "scala"],
    ["scss", "scss"],
    ["sed", "sed"],
    ["sh", "sh"],
    ["sql", "sql"],
    ["st", "st"],
    ["swift", "swift"],
    ["tcl", "tcl"],
    ["tex", "tex"],
    ["toml", "toml"],
    ["ts", "ts"],
    ["tsx", "tsx"],
    ["vb", "vb"],
    ["xml", "xml"],
    ["yaml", "yaml", " yml"],
    ["yang", "yang"]
]

func make_suffix_map()
{
    for cs in code_suffix_table {
        let code = cs[0]
        for sfx in cs[1..<cs.count] {
            suffix_map[sfx] = code
        }
    }
}
make_suffix_map()

// ############

var cmd_args = CommandLine.arguments
var program = cmd_args.removeFirst()
var program_paths = program.split(separator: "/")
var program_name = program_paths.last
var output_file = "-"

func usage() -> Never {
    print("""
        使用方法: \(String(program_name!)) [オプション] ファイル [ファイル...]
        オプション:
            -o FILE 出力するファイルを指定する(デフォルト: 標準出力 "-")
            -X      クリップボードへコピーする
        """)
    exit(1)
}

func cmdArgParse() -> Bool {
    var flag_pastboard = false
    while cmd_args.count > 0 {
        var arg = cmd_args[0]
        if arg.removeFirst() != "-" {
            break
        }
        if arg.count == 0 {
            break
        }
        cmd_args.removeFirst()
        while arg.count > 0 {
            switch (arg.removeFirst())
            {
            case "o":
                output_file = cmd_args.removeFirst()
            case "X":
                flag_pastboard = true
            default:
                usage()
            }
        }
    }
    if cmd_args.count == 0 {
        usage()
    }
    return flag_pastboard
}

func main() -> Int32 {
    let pastboard = cmdArgParse()
    var md = ""
    var md_sep = ""
    for fn in cmd_args {
        let data = FileManager.default.contents(atPath: fn)
        if data == nil {
            print("読み込み失敗: \(fn)")
            return 2
        }
        let fext = URL(fileURLWithPath: fn).pathExtension
        let code = suffix_map[fext] ?? "text"
        md += md_sep
        md += "```\(code):\(fn)\n"
        let src = String(data: data!, encoding: .utf8)!
        for var line in src.components(separatedBy: .newlines) {
            if line.prefix(3) == "```" {
                line = "\\" + line
            }
            md += line + "\n"
        }
        md += "```\n"
        md_sep = "\n"
    }

    if pastboard {
        NSPasteboard.general.clearContents()
        if !NSPasteboard.general.setString(md, forType: .string) {
            print("クリップボードへのテキスト設定に失敗しました")
            return 2
        }
        return 0
    }

    if output_file == "-" {
        print(md)
    } else if !FileManager.default.createFile(atPath: output_file, contents: md.data(using: .utf8)) {
        print("書き込み失敗: \(output_file)")
        return 2
    }
    return 0
}
exit(main())

Markdown からファイルへ
m2f.swift
import Foundation
import AppKit

// ############

var auto_filename = false
var output_dir = "."
var exclude_modes: [String] = []

// ############

class MyWebLoader {
    var sem = DispatchSemaphore(value: 0)
    var error: Error?
    var response: HTTPURLResponse?
    var data: Data?
    init(_ string: String) {
        let url = URL(string: string)!
        let req = URLRequest(url: url)
        let task = URLSession.shared.dataTask(with: req) { dat, res, err in
            self.error = err
            self.response = res as? HTTPURLResponse
            self.data = dat
            self.sem.signal()
        }
        task.resume()
        sem.wait()
    }
}

// ############

struct FileData {
    var type = ""
    var name = ""
    var data = ""
}

func parseMarkdown(_ md: String) -> [FileData] {
    var files: [FileData] = []
    var file = FileData()
    var avail = false
    var code = false
    var index = 0
    for var line in md.components(separatedBy: .newlines) {
        if line.prefix(4) == "\\```" {
            if code {
                line.removeFirst()
                file.data += "\(line)\n"
            }
            continue
        }
        if  line.prefix(3) != "```" {
            if code {
                file.data += "\(line)\n"
            }
            continue
        }
        code = !code
        if !code {
            if avail {
                files.append(file)
            }
            file = FileData()
            avail = false
            continue
        }
        let cf = line.dropFirst(3)
        let cfs = cf.split(separator: ":", maxSplits: 1)
        let ct = String(cfs[0]).trimmingCharacters(in: .whitespacesAndNewlines)
        if ct.count == 0 {
            continue
        }
        file.type = ct
        if cfs.count > 1 {
            var fn = String(cfs[1]).trimmingCharacters(in: .whitespacesAndNewlines)
            fn = fn.replacingOccurrences(of: "&#x20;", with: " ")
            fn = fn.replacingOccurrences(of: "&#x26;", with: "&")
            avail = !exclude_modes.contains(ct)
            file.name = fn
        } else {
            avail = auto_filename
            file.name = "code:\(index).\(ct)"
            index += 1
        }
    }
    return files
}

// ############

func readMarkdown(_ input: String) -> String? {
    if input == "-" {
        // 標準入力からの読み込み
        return String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8)
    }
    let input_s = input.split(separator: ":")
    switch (input_s[0]) {
    case "http":
        fallthrough
    case "https":
        // Web からの読み込み
        let hl = MyWebLoader(input)
        if hl.error != nil {
            print("読み込み失敗: \(input)")
            print("エラー: \(hl.error!.localizedDescription)")
            return nil
        }
        if hl.response != nil && !(200...209).contains(hl.response!.statusCode)  {
            print("読み込み失敗: \(input)")
            print("応答: \(hl.response!.statusCode)")
            return nil
        }
        return String(data: hl.data!, encoding: .utf8)!
    default:
        // ファイルからの読み込み
        return String(data: FileManager.default.contents(atPath: input)!, encoding: .utf8)
    }
}

// ############

var cmd_args = CommandLine.arguments
var program = cmd_args.removeFirst()
var program_paths = program.split(separator: "/")
var program_name = program_paths.last

func usage() -> Never {
    print("""
        使用方法: \(String(program_name!)) [オプション] {-X|入力}
        オプション:
            -c      console も対象にする
            -m      math も対象にする
            -n      ファイル名なしも対象にする
            -o DIR  出力するディレクトリを指定する(デフォルト: ".")
            -X      クリップボードのテキストを使用する

        入力は以下のどれか
            http(s): で始まる Web サイトのアドレス
            ファイル名 または 標準入力 "-"
        """)
    exit(1)
}

func cmdArgParse() -> Bool {
    var flag_console = false
    var flag_math = false
    var flag_pastboard = false
    while cmd_args.count > 0 {
        var arg = cmd_args[0]
        if arg.removeFirst() != "-" {
            break
        }
        if arg.count == 0 {
            break
        }
        cmd_args.removeFirst()
        while arg.count > 0 {
            switch (arg.removeFirst())
            {
            case "c":
                flag_console = true
            case "m":
                flag_math = true
            case "n":
                auto_filename = true
            case "o":
                output_dir = cmd_args.removeFirst()
            case "X":
                flag_pastboard = true
            default:
                usage()
            }
        }
    }
    if !flag_console {
        exclude_modes.append("console")
    }
    if !flag_math {
        exclude_modes.append("math")
    }
    if output_dir.last != "/" {
        output_dir += "/"
    }
    return flag_pastboard
}

func main() -> Int32 {
    var md: String?
    if cmdArgParse() {
        if cmd_args.count > 0 {
            usage()
        }
        md = NSPasteboard.general.string(forType: .string)
        if md == nil {
            print("クリップボードからテキストの取得に失敗しました")
            return 2
        }
    } else {
        if cmd_args.count < 1 {
            usage()
        }
        let input = cmd_args[0]
        md = readMarkdown(input)
        if md == nil {
            print("読み込み失敗: \(input)")
            return 2
        }
    }
    var ecd: Int32 = 0
    let pmd = parseMarkdown(md!)
    for md in pmd {
        let path = output_dir + md.name
        let pdir = (path as NSString).deletingLastPathComponent
        print(path)
        try! FileManager.default.createDirectory(atPath: pdir, withIntermediateDirectories: true)
        if !FileManager.default.createFile(atPath: path, contents: md.data.data(using: .utf8)) {
            print("書き込み失敗: \(path)")
            ecd = 2
        }
    }
    return ecd
}
exit(main())

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

[Swift] クロージャを引数として使ってみた

クロージャを関数の引数として扱うという事について纏めました。

概要 : 引数としてのクロージャ

swiftのクロージャは、関数と同じくひとまとまりの処理として使用することができます。
また、クロージャは変数/定数に代入することができるという性質もあるため、引数として取り扱うことも可能となります。

そこで簡単な計算式や関数を用いたサンプルコード1・2・3を見ながら、順を追って引数としてのクロージャとはどういうものなのかをコードリーディングしていきます。

サンプルコード1

単に値を渡すだけ

渡した値が5以上なら2倍して返す関数

// ②
func multiply(_ X: Int) -> Int {
    // ③
    print("multiply関数を実行しました")
    var answerNumber: Int = X
    // ④
    if answerNumber >= 5 {
        answerNumber *= 2
        print(answerNumber)
        return answerNumber
    } else {
        print(answerNumber)
        return answerNumber
    }
}

// ①
multiply(5)

// ~以下実行結果~
// multiply関数を実行しました
// 10

では処理の流れを追っていきます。

  1. ①により、関数multiplyが呼び出される。
    その際、引数としてInt型パラメータである5を渡している。
    引数名は省略、また型も型推論により省略されている。
  2. ②で関数multiplyの定義をしている。
    外部引数名は省略、内部引数名としてX、引数の型をInt型、戻り値の型をInt型と定義。
    ここで型を定義しているので、①の呼び出し時に型推論される。
  3. ③print文はただ単に関数内の処理に入ったことを示したかっただけ。
    var answerNumber: Int = Xで引数として受け取ったXを変数answerNumberに代入している。
  4. ④のif文で、変数answerNumberの値に手を加え出力しreturnしている。

という内容です。
今回はmultiplyの呼び出し時に5という値をそのまま引数として渡しています。
では次はこの引数部分を関数にしてみましょう。

サンプルコード2

値の部分を関数にする

  • 値に+1をするという関数
  • 5以上なら2倍して返す関数

この2つの関数を組み合わせたものになります。

// ③
func multiply(_ X: Int) -> Int {

    print("multiply関数を実行しました")
    var answerNumber: Int = X

    if answerNumber >= 5 {
        answerNumber *= 2
        print(answerNumber)
        return answerNumber
    } else {
        print(answerNumber)
        return answerNumber
    }
}
// ②
func add(_ number: Int) -> Int {
    print("add関数を実行しました")
    return number + 1
}

// ①
multiply(add(4))

// ~以下実行結果~
// add関数を実行しました
// multiply関数を実行しました
// 10

処理の流れを追っていきます。

  1. ①により、関数multiplyが呼び出される。
    その際、引数としてadd(4)という関数が設定されている。
    実行結果からも分かるように、multiplyに渡す前にaddが呼び出され処理が実行される。
  2. ②Int型を受け取り、Int型を返す処理。今回は4を受け取り+1し5を返している。
  3. ③これ以降はサンプル1と全く同じ。
    add(4)の戻り値がInt型5であり、引数としてそれを受け取っている。

サンプルコード3

値の部分をクロージャにする

処理の内容は変わらないが、実行順が変わることがポイントとなる。

// ②
func multiply(_ X: () -> Int) -> Int {

    print("multiply関数を実行しました")
    // ③
    var answerNumber: Int = X()
    // ⑤
    if answerNumber >= 5 {
        answerNumber *= 2
        print(answerNumber)
        return answerNumber
    } else {
        print(answerNumber)
        return answerNumber
    }
}

// ④
func add(_ number: Int) -> Int {
    print("add関数を実行しました")
    return number + 1
}

// ①
multiply { add(4) }

// (実行結果)
// multiply関数を実行しました
// add関数を実行しました
// 10

同じように処理の流れを見ていきます。

  1. ①により、関数multiplyが呼び出される。
    その際、引数としてクロージャ{ add(4) }が使われている。
    クロージャを引数に指定することによって、クロージャそのものを() -> Int型のパラメータとして関数に渡している

    ※実行順を見てみると、クロージャの中身であるadd関数はすぐに実行されていないことが分かります。これは引数をクロージャで包むことによって、必要があるまで処理を遅延させることができるのです。

  2. ②で関数multiplyの定義をしている。
    外部引数名は省略、内部引数名としてX、引数の型を() -> Int型、戻り値の型をInt型と定義。
    ここで型を定義しているので、①の呼び出し時に型推論される。

  3. var answerNumber: Int = X()で引数として受け取ったXを変数answerNumberに代入している。
    ここでXに代入されているクロージャ{ add(4) }のパラメータの数値が必要になる。

  4. { add(4) }によりadd関数が呼び出される。Int型を受け取り、Int型を返す処理。4を受け取り+1し5を返している。

  5. add関数の実行により、Xが持つパラメータの5変数answerNumberに代入されている状態。以降if文により分岐処理が行われる。

以上が処理の流れとなっています。

補足:実行順が逆になっていることについて

サンプル2と比べると、multiply関数とadd関数の実行順が逆になっていることが違いとなっています。
これは遅延評価と言い、処理をクロージャで包むことによって評価を遅らせることができるクロージャの仕様となります。渡した先で、実際にその引数の処理が求められた時に、クロージャが実行されます。

{}の中身の処理は一旦保留し、中身の処理ごと纏めて一つの引数として扱うという感じです。

あくまで私のイメージなのですが、数学で

(5a+2)^{2} + 7(5a+2)
a = "果てしなくめんどくさい計算式"

というような式があった時に、(5a + 2)をXなどに置き換えて

X^{2} + 7X

とすることがあると思います。
(5a + 2)の中身の計算は一旦保留して他の箇所の計算をするような時ですね。

これと同じで、{処理}Xに置き換えて引数として渡しているのが、今回のクロージャ式となります。

multiply { add(4) } の原型を考えてみよう

クロージャの基本形は以下の形
{ (引数名1: 型, 引数名2: 型, ...) -> 戻り値の型 in return 処理 }

クロージャの省略について
・型推論が可能な場合は、型の表記を省略できる。
・クロージャ内の文がひとつしかない場合はreturnを省略可能

ということを抑えたうえで、考えてみます。

引数名: なし
引数の型: ()
戻り値の型: Int
処理: add(4)

となり、原型はmultiply( {() -> Int in return add(4)} )であると考えられます。※エラーなく動きます。

ここから省略されていきますが、今回のケースだと

  1. 関数定義で型が明確に記載されている。
    型が省略され、multiply( { return add(4) } )となる。
  2. クロージャの処理文が一つのみ。
    returnが省略される。multiply( { add(4) } )
  3. トレイリングクロージャを使用し、クロージャをカッコの外に出せる。
    multiply() { add(4) }
    ※引数の一番最後がクロージャの場合のみ
  4. 引数はクロージャのみなので()も省略可能。multiply{ add(4) }

という過程でサンプルコードの形になります。

後語り

ここまでで関数の引数に、クロージャが使用された場合の扱い方や読み方について纏めさせていただきました。
クロージャは省略が可能だったり変数や定数に代入可能ということもあり、初学者にとってつまづきやすい箇所にも思えます。

どんな処理が行われているのかパッとわからない時は、省略形→原型と戻し、順を追って確認していくということをしてみると、理解しやすくなるのでおすすめです。

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

【Swift】Firebase StorageのデータをSDWebImageで表示する方法

はじめに

画像をFirebase Storageにアップロードし、
SDWebImageで取得しようとしたところ表示されず・・・。

解決するのに3時間くらいかかりました・・・。

同じような状態の方の参考になればなと思い、
備忘録がてら共有します!

環境
・Swift version 5.3
・XCode version 12.3

前提として

・Firebaseにアプリを登録していること
・CocoaPodsでFirebaseUIをインストールしていること
(Firebase/StorageやSDWebImageなど個別でも構いません)

コード

Storageに画像データを格納する際は、
UIImage型ではなくData型で格納しなければいけません。

なのでjpegData()メソッドでData型に変換しています。

imageRefは画像の格納先です。

child()メソッドで子フォルダを作成します。
profileImageフォルダを作成しさらにその下に、"uid".jpgで保存しています。

次のコードがハマりポイント①
meta.contentType = "image/jpeg"です。

これは、画像のファイルタイプをapplication/octet-streamから
"image/jpeg"に変換しています。

これをしておかないとSDWebImageで画像を表示することができません。

その後は、imageRef.putData()で画像をStorageに格納し、
imageRef.downloadURL()の結果を
.setData(["imagePath" : "\(url)"])でFirestoreに格納しています。

ViewController.swif
// 画像を.jpegData()メソッドでData型にする
let profileImage = icon.image!.jpegData(compressionQuality: 0.1)

// 格納先のパス
let imageRef = Storage.storage().reference().child("profileImage").child("\(uid!).jpg")
let meta = StorageMetadata()
// 画像データのタイプを変更する
meta.contentType = "image/jpeg"

imageRef.putData(profileImage!, metadata: meta) { (metaData, error) in
        guard metaData != nil else {
                print(error.debugDescription)
                return
        }

        imageRef.downloadURL { (url, error) in
                guard let url = url else {
                return
        }
                // FirestoreにURL格納
                self.db.collection("profileImage").document("\(self.uid!)").setData(["imagePath" : url.absoluteString])
        }
}

ここまでで、Firebase Storageに画像データを送ることができました。

私の場合は次の画面で画像を取得して表示しているのでそのまま記載します。

先ほどFirestoreに格納した値をimagePathに格納します。

Firestoreからデータを取得する方法は、
getDocument()メソッドを使用することで可能になります。

パスや値が存在した場合は、documentにデータが入るので、
document.get()のようにget( )メソッドでデータの中の値を取得します。

icon.sd_setImage()の箇所で画像を入れています。
(iconはUIImageView型です。)

NextViewController.swift
var imagePath = ""

db.collection("profileImage").document("\(uid!)").getDocument { (document, error) in
        guard let document = document else {
                return
        }
        self.imagePath = document.get("imagePath") as! String
}

icon.sd_setImage(with: URL(string: imagePath), completed: nil)

ただ、これだけだと表示されない可能性があります。

というのも、Firebase Storageのセキュリティルールに引っ掛かり
データへのアクセスが拒否されている可能性があるからです。

こちらがハマりポイント②になります。

アクセス権限の変更方法は、FIrebaseのコンソールから行います。
Storage
-> Rules
-> ルールを編集 を選択します。

デフォルトだと下記のような状態かと思います。

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
    }
  }
}

ここに/publicを追加します。

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /public/{allPaths=**} {   // この行に追加しました
      allow read, write: if request.auth != null;
    }
  }
}

さいごに

publicを追加すると、公開フォルダ?的な感じになると思うので、
それだとセキュリティ的に不味い場合はやめた方がいいかもしれません。

おそらくですが、これでうまくいくと思います!
私の場合はこれで画像を取得することができました・・・。

冷静に考えれば分かることだったかもしれませんが、
なかなか気づかないものですね(笑)

以上、最後までご覧いただきありがとうございました。

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