20200116のSwiftに関する記事は15件です。

【swift5】ストップウォッチタイマーの作り方

キャプチャ

スクリーンショット 2020-01-16 21.44.33.png

ソース全部

githubにもアップしております!

https://github.com/sventouz/swift_timer

import UIKit
class ViewController: UIViewController {
    var OurTImer = Timer()
    var TimerDisplayed = 0
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var start: UIButton!
    @IBOutlet weak var pause: UIButton!
    @IBOutlet weak var reset: UIButton!

    @IBAction func startButton(_ sender: Any) {
        OurTImer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(Action), userInfo: nil, repeats: true)
    }
    @IBAction func pauseButton(_ sender: Any) {
        OurTImer.invalidate()
    }
    @IBAction func resetButton(_ sender: Any) {
        OurTImer.invalidate()
        TimerDisplayed = 0
        label.text = "0"
    }
    @objc func Action() {
        TimerDisplayed += 1
        label.text = String(TimerDisplayed)
    }

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

ソースの解説

    var OurTImer = Timer()
    var TimerDisplayed = 0
    @IBOutlet weak var label: UILabel!

Timer()のインスタンス化
TimerDisplayedは秒数を数えてくれている変数
あとは普通の変数

    @IBAction func startButton(_ sender: Any) {
        OurTImer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(Action), userInfo: nil, repeats: true)
    }
    @IBAction func pauseButton(_ sender: Any) {
        OurTImer.invalidate()
    }
    @IBAction func resetButton(_ sender: Any) {
        OurTImer.invalidate()
        TimerDisplayed = 0
        label.text = "0"
    }
    @objc func Action() {
        TimerDisplayed += 1
        label.text = String(TimerDisplayed)
    }

スタートボタンを押した時

startButtonがスタートボタンを押した時の挙動

    @IBAction func startButton(_ sender: Any) {
    }

これ

OurTImer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(Action), userInfo: nil, repeats: true)

//の中の

selector: #selector(Action)

は下で定義しているこれ

    @objc func Action() {
        TimerDisplayed += 1
        label.text = String(TimerDisplayed)
    }
scheduledTimer

で1秒ずつTimerDisplayedが1づつ増えていっている

ポーズボタンを押した時

    @IBAction func pauseButton(_ sender: Any) {
        OurTImer.invalidate()
    }

タイマーを止める処理

リセットボタンを押した時

@IBAction func resetButton(_ sender: Any) {
     OurTImer.invalidate()
     TimerDisplayed = 0
     label.text = "0"
}

タイマーを止める処理をして
表示を0にする

まとめ

これで完成!

参考

https://www.youtube.com/watch?v=z2Jq5U-stag

最近はYoutubeにも優良な情報があってプログラミングを学びたい人には天国ですね。

(変な情報もたくさんあるのでお気をつけを。)

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

【swift】ユーザーが任意のテーマカラーを設定できる機能

前置き

参考ページのまとめ集。

環境

xcode 11.3
swift 5.1.3
CocoaPods 1.8.4

機能要件

テーマカラー選択機能
テーマカラー反映機能

仕様

  • テーマカラー設定ボタンを押すと設定ページへ
  • 設定ページには、色が並んでいる
  • 色をクリックすると、設定ページの背景色が変わる
  • 決定ボタンを押すと、選択された色がテーマカラーとなる
  • もとの画面に戻ったのち、テーマカラーが反映されている

スクリーンショット 2020-01-16 21.53.15.png
スクリーンショット 2020-01-16 21.53.20.png
スクリーンショット 2020-01-16 21.53.25.png
スクリーンショット 2020-01-16 21.53.28.png

設計

storyboard上に要素の追加

  • コントローラー
  • カラーパレット
  • 決定ボタン

カラーパレットの設置

以下の記事に従ってcollectionViewを作成

qiita : 【Swift・iOS】XcodeのCollectionViewの使い方。タイル型(カード型)のレイアウト方法を解説

以下の記事を参考に、タップアクションを追加

qiita : UICollectionViewのセルの強調・選択時にセルの見た目を変化させる

以下の記事に従って形を整形

qiita : CollectionViewの基礎
qiita : Swift UIViewの一部を角丸にしたい

選択カラーの保存

userDefaultsの設定

テーマカラー格納用にクラスを作っておく

【swift】userDefaultsへの書き込み・読み込みを簡易化する

UIColorの変換

userDefaultsにカラーを保存できるように、UIColorをData型に変換する

Swift 3でUserDefaultsにUIColorを保存する方法

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

VaporからDockerで立てたMySQL8に接続し、Leafで表示する

前回の記事 VaporからDockerで立てたMySQL8に接続する では Vapor から Docker に立てた MySQL に接続するところまで実装しました。
今回の記事では、接続したデータベースから値を取り出し、テンプレートエンジンである Leaf で使っていこうと思います。

リポジトリはこちら O-Junpei/sommelier-vapor

まず Docker で立てた MySQL にテーブルを作成し、検証用のデータを追加します。
IT企業の技術ブログをまとめを作りたいので、企業ブログの記事の情報が入る article テーブルを作成します。

01_ddl.sql
create table article(
    article_id int(11) not null auto_increment,
    title varchar(255),
    article_url varchar(255),
    site_name varchar(255),
    site_url varchar(255),
    primary key (article_id)
);

サンプルデータとして、以下の6記事を追加します。

02_data.sql
SET CHARACTER_SET_CLIENT = utf8mb4;
SET CHARACTER_SET_CONNECTION = utf8mb4;

insert into article (article_id, title, article_url, site_name, site_url)
values (1, 'Amazon Elasticsearch ServiceをつかったRDSのスロークエリの集計と監視', 'https://techlife.cookpad.com/entry/2019/12/27/000000', 'クックパッド開発者ブログ', 'https://techlife.cookpad.com');

insert into article (article_id, title, article_url, site_name, site_url)
values (2, 'プロと読み解くRuby 2.7 NEWS', 'https://techlife.cookpad.com/entry/2019/12/25/121834', 'クックパッド開発者ブログ', 'https://techlife.cookpad.com');

insert into article (article_id, title, article_url, site_name, site_url)
values (3, 'センサクッキング 技術検証とユーザー体験検証', 'https://techlife.cookpad.com/entry/2019/12/18/110000', 'クックパッド開発者ブログ', 'https://techlife.cookpad.com');

insert into article (article_id, title, article_url, site_name, site_url)
values (4, '[小ネタ]WorkSpacesのディレクトリが消せないし理由も分からない?そんな時はAWS Directory Serviceを見てみよう', 'https://dev.classmethod.jp/cloud/aws/how-to-delete-ad-for-workspaces-and-find-the-cause/', 'クラスメソッド発「やってみた」系技術メディア | Developers.IO', 'https://dev.classmethod.jp');

insert into article (article_id, title, article_url, site_name, site_url)
values (5, '[レポート] 可観測性は AI ・メトリクス・ログの幸せな結婚を夢見るか? AIOps の雄、Moogsoft の CEO が語る #AIM310 #reinvent', 'https://dev.classmethod.jp/cloud/aws/201912-report-reinvent-2019-aim310/', 'クラスメソッド発「やってみた」系技術メディア | Developers.IO', 'https://dev.classmethod.jp');

insert into article (article_id, title, article_url, site_name, site_url)
values (6, '[レポート] Container Insight, FireLens, AppMesh を使ってコンテナ環境 (ECS/EKS/Fargate) の可観測性を向上させる #CON328 #reinvent', 'https://dev.classmethod.jp/cloud/aws/201912-report-reinvent-2019-con328/', 'クラスメソッド発「やってみた」系技術メディア | Developers.IO', 'https://dev.classmethod.jp');

01_ddl.sql02_data.sqlinitdb ディレクトリに入れ、initdb ディレクトリ は /docker-entrypoint-initdb.d ディレクトリ としてマウントするようにしました。
こうすることで、MySQL コンテナ作成時に2つの .sql ファイルがコンテナの新規作成時に実行されます。

docker-compose.yml
services:
  db:
    image: mysql:8.0.18
    container_name: sommelier-mysql
    environment:
      MYSQL_ROOT_PASSWORD: sommelier
      MYSQL_DATABASE: sommelier_local
      MYSQL_USER: sommelier
      MYSQL_PASSWORD: sommelier
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - 3306:3306
    volumes:
      - ./Database/initdb:/docker-entrypoint-initdb.d

参考: DockerHub MySQL Initializing a fresh instance

article テーブルに対応する Article クラスを作成します。
entity プロパティにテーブル名を設定し、カラム名と同名のプロパティを作成します。

Article.swift
import FluentMySQL
import Vapor

struct Article: Decodable, MySQLModel {
    var id: Int?
    static let entity = "article"  // テーブル名

    var article_id: Int?
    var title: String?
    var article_url: String?
    var site_name: String?
    var site_url: String?
}

extension Article: Content { }
extension Article: Migration { }

CodingKeys を使用することで、カラム名と異なるプロパティ名にすることもできます。

Article.swift
import FluentMySQL
import Vapor

struct Article: Decodable, MySQLModel {
    var id: Int?
    static let entity = "article"  // テーブル名

    var articleId: Int?
    var title: String?
    var articleUrl: String?
    var site_name: String?
    var siteUrl: String?

    private enum CodingKeys: String, CodingKey {
        case articleId = "article_id"
        case title = "title"
        case articleUrl = "article_url"
        case site_name = "site_name"
        case siteUrl = "site_url"
    }
}

extension Article: Content { }
extension Article: Migration { }

参考:
Models - Vapor Docs
Option to convert to snake_case

作成した Articleクラスの設定を configure.swift で行います。

configure.swift
/// Configure migrations
var migrations = MigrationConfig()
migrations.add(model: Article.self, database: .mysql)
services.register(migrations)

このこと関連する内容の記事を書いてくださった方がおりますので紹介します。
本番運用するアプリでモデルの自動マイグレーションを使ってはいけない  

これで MySQL のデータを簡単に扱うことができます。

file.png

ArticleController.swift
import Vapor
import FluentMySQL

final class ArticleController {
    static func blogs(req: Request) throws -> Future<[Blog]> {
        let cookpadArticles: Future<[Article]> = Article.query(on: req).filter(\.site_url == "https://techlife.cookpad.com") .all()
        let classmethodArticles: Future<[Article]> = Article.query(on: req).filter(\.site_url == "https://dev.classmethod.jp") .all()

        let blogs = map(cookpadArticles, classmethodArticles) { (cookpadArticles, classmethodArticles) -> ([Blog]) in
            let cookpadBlog = Blog(name: "クックパッド開発者ブログ", url: "https://techlife.cookpad.com", articles: cookpadArticles)
            let classmethodBlog = Blog(name: "クラスメソッド発「やってみた」系技術メディア | Developers.IO", url: "https://dev.classmethod.jp", articles: classmethodArticles)
            return [cookpadBlog, classmethodBlog]
        }
        return blogs
    }
}
routes.swift
import Vapor

public func routes(_ router: Router) throws {

    // 2chmm page
    router.get { req -> Future<View> in
        return try req.view().render("2chmm", [
            "blogs": ArticleController.blogs(req: req)
        ])
    }
}
2chmm.leaf
#set("title") { 技術ブログまとめ }

#set("body") {
    <ul>
    #for(blog in blogs) {
        <li>
          <a href="#(blog.url)"><h1>#(blog.name)</h1></a>
          <ul>
            #for(article in blog.articles) {
              <li><a href="#(article.article_url)">#(article.title)</h1></a></li>
            }
          </ul>
        </li>
    }
    </ul>
}

#embed("base")

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

【初心者向け】Showing All Issues "アプリ名" requires a provisioning profile. Select a provisioning profile in the Signing & Capabilities editor.が出た時の対処法。

解決法

プロビジョニングファイルが設定されていない。
Xcodeのプロジェクトファイルにある、
Signing & Capabilities
→Automatically manage singing
にチェックを入れてTeamの設定をするだけで解決します。
スクリーンショット 2020-01-16 11.37.26.png

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

【初心者向け】Could not launch “アプリ名” Verify the Developer App certificate for your account is trusted on your device. が出た時の対処法。

一番最初にXcodeでRunした時にこのようなエラーが出ることがあり、まとまってる記事がなかったので書きました。

Could not launch “アプリ名”
Verify the Developer App certificate for your account is trusted on your device. Open Settings on “端末名” and navigate to General -> Device Management, then select your Developer App certificate to trust it.
Internal launch error: process launch failed: Security

解決法

インストール先の端末で設定を追加する。

  • iOS12
    一般→プロファイルとデバイス管理→デベロッパAppを選択→信頼を選択

  • iOS13
    一般→デバイス管理→デベロッパAppを選択→信頼を選択

※スクショはiOS12です。

「一般」を選択
1-2.png

「プロファイルとデバイス管理」を選択
2-2.png

「デベロッパAppを選択」を選択
3-2.png

「信頼」を選択
4-2.png

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

【swift】userDefaultsへの書き込み・読み込みを簡易化する

環境

xcode 11.3
swift 5.1.3
CocoaPods 1.8.4

方針

@noby111さんの記事「Swifterを自分のアプリに組み込む方法」を参考にしていた時に以下の方法がさらっと用いられていていいなと思ったので、これだけでまとめました。

AppStatusクラスを作って、クラス変数にアクセスする方法でuserDefault内のデータにアクセスできるようにする

let appData = AppStatus()

//データの入力
appData.username = "test user"
// データの出力
print(appData.username)
出力
test user

実装

AppStatus.swiftファイルを新たに作成し、以下を記述

AppStatus.swift
import Foundation
import UIKit

class AppStatus {
    var userdefault = UserDefaults.init(suiteName: "app_status")!

    // usernameの設定
    var username : String? {
        get {
            if let username : String = userdefault["username"] {
                return username
            } else {
                return nil
            }
        }

        set {
            userdefault["username"] = newValue
        }
    }
}

extension UserDefaults {
    subscript<T: Any>(key: String) -> T? {
        get {
            if let value = object(forKey: key) {
                return value as? T
            } else {
                return nil
            }
        }

        set(_newValue) {
            if let newValue = _newValue {
                set(newValue, forKey: key)
            } else {
                removeObject(forKey: key)
            }
            synchronize()
        }
    }
}

usernameの部分と型を変更すればuserDefaulに保存可能な任意のデータ型を扱うことができる。
これ便利。

userDefaultsの使い方

ちなみに、userDefaultとはなんぞや、とかどうやって使うの、とかは以下の記事が参考になりました。

qiita : swift4 超初心者向け!"UserDefaults"の使い方とか 忘備録

qiita : UserDefaultsの使い方

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

MessageKitをクローンしたがビルドができなかった話

はじめに

MessageKitとはiOSアプリでLINEのようなチャット画面を簡単に作成することができるframeworkであり、cocoapodsでinstallできる。
ただ使い方がよく分からなかったので、クローンしてサンプルプロジェクト(MessageKit/Example/ChatExample.xcworkspace)を触ろうとしたがビルドが通らなかった。

エラー文

.../MessageKit/Example/Pods/Target Support Files/ Pods-ChatExample/Pods-ChatExample.debug.xcconfig: unable open file (in target "ChatExample" in project "ChatExample") (in target 'ChatExample')

解決策

ChatExample.xcworkspaceを削除し、

pod install

を実行。失敗。エラー文は以下

Fetching podspec for `MessageKit` from `../`
[!] CocoaPods could not find compatible versions for pod "InputBarAccessoryView":
  In snapshot (Podfile.lock):
    InputBarAccessoryView (= 4.2.2)

  In Podfile:
    MessageKit (from `../`) was resolved to 3.0.0, which depends on
      InputBarAccessoryView (~> 4.2.2)

None of your spec sources contain a spec satisfying the dependencies: `InputBarAccessoryView (= 4.2.2), InputBarAccessoryView (~> 4.2.2)`.

よく分からんが要はキャッシュを手動で更新してやる必要があるらしい。(参考pod install でライブラリの最新バージョンが入らないときの対処)

pod repo update
pod install

通った。

結果

見れた。めでたしめでたし。
スクリーンショット 2020-01-16 17.31.35.png

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

Swift: map()でゴチャついた処理をreduce(into:)でスッキリさせる

はじめに

reduce(into:)はちょっと理解あまりしていなく使用を避けていた節があり、map系でゴリゴリしていたらこれ使えるよってアドバイスもらって使い方がわかったので備忘録。

reduce(into:)の用途

official
https://developer.apple.com/documentation/swift/array/3126956-reduce


想定

APIレスポンスを元にTableViewにて、セクションを切ってアイテムをリスト表示させたいケースがありました。

image.png

Header
SectionHeaderは別途使用していたのと、viewをいじいじしたCustomViewを使用したかったなどで、headerCustomCellを使用したかった。

Cell
普通にcustomCell

こんな感じのリストがレスポンスとして返ってくるとします。

struct List {
    let kind: String
    let items: [Item]
}

struct Item {
    let nameJP: String
    let nameEN: String
}

let lists: [List] = [List(kind: "veges", items: [Item(nameJP: "ジャガイモ", nameEN: "potato"),
                                               Item(nameJP: "トマト", nameEN: "tomato"),
                                               Item(nameJP: "タマネギ", nameEN: "onion"),...]),
                    List(kind: "fruits", items: [Item(nameJP: "レモン", nameEN: "lemon"),
                                                 Item(nameJP: "リンゴ", nameEN: "apple"),
                                                 Item(nameJP: "ミカン", nameEN: "orange"),...]),
                    List(kind: "drinks", items: [Item(nameJP: "水", nameEN: "water"),
                                                 Item(nameJP: "ソーダ", nameEN: "sparkling"),
                                                 Item(nameJP: "コーラ", nameEN: "coke"),...]),
                    ...]

これをこうしたい。

image.png

なんならアイテムが無ければこうもしたい。

image.png

TableView.CellForRowAtで操作しやすくするためにベースとなる配列を作成したい、というところ。

条件

  • ヘッダーの文字列 + それに対応したアイテムリスト を作成したい
  • アイテムの数に比例したセル数を作成する必要があるので、個数も対応したい
  • アイテムがない場合はヘッダーも要らないので、そのセクションは排除したい

TableViewの実装方法にもよりますが、割愛します。
これを実現させるために以下のような配列が必要。(1つの案として)

enum Section {
    case header(String)
    case names([String])
}

var sections: [Section] = [.header("veges"), .names(["ジャガイモ", "トマト", "タマネギ"]),
                           .header("fruits"), .names(["レモン", "リンゴ", "ミカン"]),
                           .header("drinks"), .names(["水", "ソーダ", "コーラ"]),...]
// アイテムがなければ省いたリスト
var sections: [Section] = [.header("veges"), .names(["ジャガイモ", "トマト", "タマネギ"]),
                           .header("drinks"), .names(["水", "ソーダ", "コーラ"]),...]

こうすることで、cellForAtRowではswitch sections[indexPath.section]を切って別々のカスタムセルを付与できるし、
配列なのでindexPath.row/sectionの数も合います。

では作ろうということで、以下を実装しました。

コード

前置き長くなりましたがreduce(into:)の本題。
最初はmap系駆使してやりました。

第一形態

var sections: [Section] {
    let array: [(kind: String, namesJP: [String])] = lists.compactMap { list in
        if !list.items.isEmpty {
            return (kind: list.kind, namesJP: list.items.map { $0.nameJP })
        }
        return nil
    }
    return array.flatMap { [.header($0.kind), .names($0.namesJP)] }
}
  • kind & namesJP をセットで操作したいのでタプル型の配列を作る
  • アイテムがない場合はセクションを排除したいので、isEmpty判定でなければnilを返す
  • compactMapnil掃除
  • 一時的に作成した配列と、最終的に使用したい配列の型が異なるのでflatMapで欲しいものだけ取り出す

これでも動きますが、マップマップしてちょっと騒がしい。

ということで提案いただいたその1が以下。

第二形態

var sections: [Section] {
    return lists.flatMap { list -> [Section] in
        if !list.items.isEmpty {
            return [.header(list.kind), .names(list.items.map { $0.nameJP })]
        } else {
            return []
        }
    }
}

一時的な配列を作成しないで一気に作成。少し静かになりました。

しかし、これはあと一回進化を残しています。その意味が分か
ここでreduce(into:)を使用します。

第三形態

var sections: [Section] {
    return lists.reduce(into: [Section]()) { sections, list in
        sections.append(contentsOf: [.header(list.kind), .names(list.items.map { $0.nameJP })])
    }
}

なんということでしょう、ほぼほぼ2行になりました。
凝縮していますが、まだリーダブルの範囲です。慣れればサクッとリーディングできそうです。

reduce(into:)の使い方

struct List {
    let kind: String
    let items: [Item]
}

struct Item {
    let nameJP: String
    let nameEN: String
}

let lists: [List] = [List(kind: "veges", items: [Item(nameJP: "ジャガイモ", nameEN: "potato"),
                                               Item(nameJP: "トマト", nameEN: "tomato"),
                                               Item(nameJP: "タマネギ", nameEN: "onion"),...]),
                    List(kind: "fruits", items: [Item(nameJP: "レモン", nameEN: "lemon"),
                                                 Item(nameJP: "リンゴ", nameEN: "apple"),
                                                 Item(nameJP: "ミカン", nameEN: "orange"),...]),
                    List(kind: "drinks", items: [Item(nameJP: "水", nameEN: "water"),
                                                 Item(nameJP: "ソーダ", nameEN: "sparkling"),
                                                 Item(nameJP: "コーラ", nameEN: "coke"),...]),
                    ...]

// before
var sections: [Section] {
    let array: [(kind: String, namesJP: [String])] = lists.compactMap { list in
        if !list.items.isEmpty {
            return (kind: list.kind, namesJP: list.items.map { $0.nameJP })
        }
        return nil
    }
    return array.flatMap { [.header($0.kind), .names($0.namesJP)] }
}

// after
var sections: [Section] {
    // 型推論があるので into: [] だけでもOK、よりスッキリする
    // 左は Result; 最終的に返る型 右は element; 配列の値
    return lists.reduce(into: []) { sections, list in
        // あとは Resultの形に対してどんな処理を行いたいか記述
        sections.append(contentsOf: [.header(list.kind), .names(list.items.map { $0.nameJP })])
    }
}

print(sections)
// [__lldb_expr_4.Section.header("veges"), __lldb_expr_4.Section.names(["ジャガイモ", "トマト", "タマネギ"]),
// __lldb_expr_4.Section.header("fruits"), __lldb_expr_4.Section.names(["レモン", "リンゴ", "ミカン"]),
// __lldb_expr_4.Section.header("drinks"), __lldb_expr_4.Section.names(["水", "ソーダ", "コーラ"])]

こういう時にreduce(into:)使えるかも?と閃くキッカケとして

  • 何かをベースに配列や辞書(ベースとは別の型)を作成したい時
  • nilの場合、該当する値を排除したい時(nilを許容しない場合)

flatMap / compactMapなどカリカリする時があれば閃ければいいです。
APIレスポンスを操作するときなどマッピングする機会はたくさんあると思うので使い所はありそうですし、スッキリしていいですね。

まとめ

マッピング系処理のネタが増えてよかった。
読みもそうだけど、実装できるか判断するまで慣れが必要そう。

練習します。

参考

https://qiita.com/koher/items/17636e95e18e529e5b9b

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

swift version切り替える

Swiftバージョンを切り替える方法をメモします。

swiftenvのインストール

https://swiftenv.fuller.li/en/latest/installation.html

git cloneとHomebrewでのインストール方法を記載してくれています。
今回はHomebrewでインストール。

まずは、インストール

brew install kylef/formulae/swiftenv`

次に.bash_profileにパスの追加

echo 'if which swiftenv > /dev/null; then eval "$(swiftenv init -)"; fi' >> ~/.bash_profile`

ローカルのSwiftバージョン確認

$ swiftenv versions
* system
  5.1.3

ローカルのSwiftバージョンの変更

Swift バージョンのインストール

$ swiftenv install 5.0.1
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  305M  100  305M    0     0  2036k      0  0:02:33  0:02:33 --:--:-- 3658k
Password:
installer: Package name is Swift Open Source Xcode Toolchain
installer: Installing at base path /
installer: The install was successful.
5.0.1 has been installed.

新しいSwiftバージョンをインストールしたら、こちらのコマンドの実行が必要のよう。

$ swiftenv rehash

ローカルのSwiftバージョン確認

swiftenv versions
* 5.0.1
  5.1.3

無事に目的のSwift バージョンに切り替わってました:clap:

メモ:
Carthageが利用するSwiftバージョンが切り替わらない...誰か解る人いたら教えてください。

https://github.com/kylef/swiftenv/issues/145

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

TableView と delegate を理解する(したい)

対象

Swift 初心者向け🐤
参考資料程度に呼んでください!

どんなときに読むの?

TableView と delegate にハマったとき。
TableView 難しいよ〜 となったとき。

TableView と delegate のはまりどころ

  • 更新されない
    • tableView.reloadData() を呼んでいない
    • 配列に追加した後に、tableView.reloadData() を呼んでいない
  • 表示されない
    • tableView.dataSource = self 書いていない.
    • numberOfInSection で return 0 になっている
    • class 〇〇ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate にしていない
  • アプリが落ちた
    • cell の名前を確認。間違ってないか
    • tableView.register で cell を登録しているか
    • 配列の範囲外(配列の要素数)の外にアクセスしているか
    • ( 配列は、0, 1, 2, 3... と数が増える。 )
  • cell をタップしても反応しない
    • tableView.delegate = self を書いていない
    • class 〇〇ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate にしていない
    • didSelectRowAt を呼んでない
  • エラーが消えない
    • class 〇〇ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate にしていない

参考になる資料

TableView

delegate

いろんなアプローチの解説があるので自分に合うものを探しましょう!

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

App Store Connect の Missing Compliance をなくす方法

問題

TestFlight配信するために IPAファイルをApp Store Connectへアーカイブしているのですが、アーカイブ後の処理(長い)が終わったあとで毎回「Missing Compliance」というワーニングがでて、毎度アーカイブ後にApp Store Connect開いて対処する必要がありました。

Missing Complianceとは

これが何のかというと、独自の暗号方式を利用している場合は適切なドキュメントを提出する必要がありますよ、という話で、独自の暗号化しないならNOを選ぶという事になると思います。それなら毎回聞かれたくないですよね。

方法

Info.plist に 下記を加えるだけです。

<key>ITSAppUsesNonExemptEncryption</key>
<false/>

Xcodeからの追加だと、Keyの名前は App Uses Non-Exempt Encryptionとなるので気をつけましょう。

参考

https://stackoverflow.com/a/35842359/6891441

https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption

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

【Swift】Chartsでオシャレなグラフを描画してみる( 棒グラフ編 )

はじめに

Chartsというライブラリはご存知でしょうか?
とても有名なライブラリで、iOSでグラフを描画しようと思ったことがある人なら聞いたことや使ったことがあると思います。

Chartsはグラフ自体を非常に細かくカスタマイズすることができる素晴らしいライブラリです✨
今回は忘備録も兼ねてChartsを使って今時でオシャレな棒グラフを作ってみたいと思います。

オシャレな棒グラフって?

普段実装ばかりでデザインのことには疎いので、
まずは今時でオシャレな棒グラフのデザインをDribbbleBehanceで調査してみました。
その結果以下のような棒グラフが流行っているような感じがしました。
Figmaでパパッと作ったので数値などは適当です。

Y軸の値表示なし Y軸の値表示あり
Bar chart ( disable grid ).png Bar chart ( enable grid ).png

ポイントをまとめるとこんな感じになりました。

  1. X軸の縦線は表示しない
  2. バーの右上と左上は角丸
  3. グラフの平均を表す線があって、点線で表示
  4. Y軸の値を他のラベルなどに表示する場合はグラフに表示しない

4は以下のようなUIの場合、グラフにはY軸の値を表示しない代わりに、グラフをタップするとグラフの上に表示されているラベルにY軸の値が表示されるようなイメージです。

スクリーンショット 2020-01-14 15.29.01.png

早速作っていきたいところですが、一点問題があります。

棒グラフの角丸はサポートされていない

現在、Chartsでは棒グラフの角丸はサポートされていません。
PRは存在しますが、色々問題があってマージされていない状態です。

さすがにフォークして直すのは時間がかかりそうだったので、角丸は省略したものを作成します。
個人的には角丸がなくても結構それっぽくなるので、これはこれでありだと思っています。

実際に作ってみる

それでは実際に作っていきます。

Chartsのインストール

ChartsはCocoaPodsとCarthageに対応しています。
自分はCarthageでインストールしました。詳しくは公式のCarthage Installを参考にしてください。

BarChartViewを追加する

Chartsのグラフを描画するChartViewInterface Builderに対応しているのでStoryboardから追加することができます。

今回は棒グラフを描画したいので一枚UIViewを用意して、Custom ClassBarChartViewModuleChartsを設定します。

スクリーンショット 2020-01-15 16.12.09.png

あとはいつも通りViewControllerにOutlet接続すればコードでいじれるようになります。

import UIKit
import Charts

class ViewController: UIViewController {

    @IBOutlet weak var barChartView: BarChartView!

    // ...
}

グラフに表示するデータについて

Chartsでは表示するデータをChartDataに変換する必要があります。
変換したChartDataChartViewのもつdataプロパティにセットすることでデータが描画されます。
以下では適当なIntの配列をBarChartDataに変換してグラフに表示しています。

let rawData: [Int] = [20, 50, 70, 30, 60, 90, 40]
let entries = rawData.enumerated().map { BarChartDataEntry(x: Double($0.offset), y: Double($0.element)) }
let dataSet = BarChartDataSet(entries: entries)
let data = BarChartData(dataSet: dataSet)
barChartView.data = data

デフォルトの状態でこれを表示するとこんな感じになります。

デフォルト
スクリーンショット 2020-01-15 16.26.26.png

X座標軸の設定

X座標軸関連の設定はxAxisプロパティに対して設定します。
デフォルトの状態では余分な線などが表示されていたりするので、X軸のラベルの位置、軸の線の表示、グリッドの非表示を行っています。

// X軸のラベルの位置を下に設定
barChartView.xAxis.labelPosition = .bottom
// X軸のラベルの色を設定
barChartView.xAxis.labelTextColor = .systemGray
// X軸の線、グリッドを非表示にする
barChartView.xAxis.drawGridLinesEnabled = false
barChartView.xAxis.drawAxisLineEnabled = false

右側のY座標軸の設定

Y座標軸は左右それぞれ設定することができます。
右側のY座標軸関連の設定はrightAxisプロパティに設定します。
今回は右側のY座標軸は表示しないので無効にします。

// 右側のY座標軸は非表示にする
barChartView.rightAxis.enabled = false

左側のY座標軸の設定

左側のY座標軸関連の設定はleftAxisプロパティに設定します。
ここはY軸の値を表示するかしないかで変わってきますが、とりあえずY軸の値を表示したパターンを作成します。

デフォルトではY座標の最小値がグラフに表示しているデータによって決定されてしまうので、axisMinimumに0を設定して0始まりになるように設定したり、ラベルの色や数、グリッドや軸線の表示に関する設定をしています。

// Y座標の値が0始まりになるように設定
barChartView.leftAxis.axisMinimum = 0.0
barChartView.leftAxis.drawZeroLineEnabled = true
barChartView.leftAxis.zeroLineColor = .systemGray
// ラベルの数を設定
barChartView.leftAxis.labelCount = 5
// ラベルの色を設定
barChartView.leftAxis.labelTextColor = .systemGray
// グリッドの色を設定
barChartView.leftAxis.gridColor = .systemGray
// 軸線は非表示にする
barChartView.leftAxis.drawAxisLineEnabled = false

凡例の設定

デフォルトではグラフの凡例が表示されているので非表示にします。

barChartView.legend.enabled = false

平均の線の設定

平均の線はLimitLineを使って表示します。
以下では線の色と点線の設定を行っています。lineDashLengthsは複数の値を入れることで不規則な形の点線を作ることも可能です。

let avg = rawData.reduce(0) { return $0 + $1 } / rawData.count
let limitLine = ChartLimitLine(limit: Double(avg))
limitLine.lineColor = .systemOrange
limitLine.lineDashLengths = [4]
barChartView.leftAxis.addLimitLine(limitLine)

LimitLineleftAxisもしくはrightAxisに対して追加するので、追加する対象のenablefalseになっていると表示されないので注意してください。

ここまでで以下のような見た目になりました。だいぶそれっぽくなったと思います。

デフォルト 設定後
スクリーンショット 2020-01-15 16.26.26.png スクリーンショット 2020-01-16 10.04.58.png

棒グラフの色と値の表示の設定

最後に棒グラフの色とグラフの上に値が表示しないように設定します。
これらの設定はChartDataSetに対して設定します。

let dataSet = BarChartDataSet(entries: entries)
dataSet.drawValuesEnabled = false
dataSet.colors = [.systemBlue]

以上で完成です。
最終的にこんな感じになりました。

デフォルト 完成形
スクリーンショット 2020-01-15 16.26.26.png スクリーンショット 2020-01-16 10.26.29.png

さいごに

Chartsで今時でオシャレな棒グラフを作ってみましたがいかがだったでしょうか?
他にもグラフをアニメーションさせたり、ハイライトした時にマーカーを表示できたり、X軸のラベルの表示をカスタマイズできるなど、まだまだカスタマイズできるので気になった方はぜひ試してみてください。公式のサンプルで一通り使い方はマスターできると思います。

また、今回作ったグラフはサンプルとしてQiitaChartsSampleにあげてあるので、よければ参考にしてください。

分かりにくい部分や間違いがあれば指摘していただけると嬉しいです🙇‍♂️
最後まで読んでいただきありがとうございました。

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

Property Requirements of Protocol in Swift

這篇是關於 { get }{ get set } 的基礎筆記。

問題點

最近發現為什麼被定義成 { get } 的屬性為什麼還可以被 assign 值覺得不可思議,因此再次幫自己舉例釐清到底為什麼。

而這個原因(可以被 assign)雖然看起來理所當然,但是在實踐 pop 的時候可能因為覺得太過理所當然會不小心忽略,或是在指定成 protocol 型別之前就先被拿來當成繞過 protocol 限制的方法,姑且不論好壞,就先決定記下來。

像是以這樣的定義

protocol A: class {
    var a: String! { get }
}

class AClass: A {
    var a: String!
    init() {}
}

發現可以被 assign 值的實作是這樣:

let a1 = AClass()
a1.a = "some text"

這樣 Xcode 的編輯器不會抱怨, runtime 也一樣照樣過。
原因就是在 a1 雖然用了有 A 成分的 AClass ,但是對編譯器來說他還是 AClass ,protocol A 的優先度反而不是更高的。

不過如果這樣寫,明確的指定我的變數就是 protocol A

let a2: A = AClass()
a2.a = "some text"

Xcode 就會如期待的抱怨了:

image.png

環境

Xcode 11.3.1, Playground

參考

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

為什麼 { get } 可以 assign?!

這篇是關於 { get }{ get set } 的基礎筆記。

問題點

最近發現為什麼被定義成 { get } 的屬性為什麼還可以被 assign 值覺得不可思議,因此再次幫自己舉例釐清到底為什麼。

而這個原因(可以被 assign)雖然看起來理所當然,但是在實踐 pop 的時候可能因為覺得太過理所當然會不小心忽略,或是在指定成 protocol 型別之前就先被拿來當成繞過 protocol 限制的方法,姑且不論好壞,就先決定記下來。

像是以這樣的定義

protocol A: class {
    var a: String! { get }
}

class AClass: A {
    var a: String!
    init() {}
}

發現可以被 assign 值的實作是這樣:

let a1 = AClass()
a1.a = "some text"

這樣 Xcode 的編輯器不會抱怨, runtime 也一樣照樣過。
原因就是在 a1 雖然用了有 A 成分的 AClass ,但是對編譯器來說他還是 AClass ,protocol A 的優先度反而不是更高的。

不過如果這樣寫,明確的指定我的變數就是 protocol A

let a2: A = AClass()
a2.a = "some text"

Xcode 就會如期待的抱怨了:

image.png

環境

Xcode 11.3.1, Playground

參考

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

(swift)Loafで、サクッとエラーメッセージを表示する

Loafって?

(実装例)
20200116_021428.GIF

Inspired by Android's Toast,
Loaf is a Swifty Framework for Easy iOS Toasts
(引用: https://github.com/schmidyy/Loaf)

つまり、
ios版のAndroid's Toastのようです。

ちなみに、Android's Toastとは、以下です。

トーストは、操作に関する簡単なフィードバックを小さなポップアップに表示します。 トーストでは、メッセージの表示に必要なスペースのみを使用します。現在のアクティビティは表示されたままになり、引き続き操作することができます。トーストはタイムアウト後に自動的に消えます。
(引用: https://developer.android.com/guide/topics/ui/notifiers/toasts?hl=ja)!

Loafを使うことで、超カンタンにエラーメッセージやアラートなどを実装することができます。

Loafの使い方

Cocoapods
まず、pod 'Loaf'を追加して、pod installしてください。

そして、以下のコードを記述してください。

Loaf("メッセージを表示", sender: self).show()

以上です。(カンタンスギ)

応用

スタイルをカスタマイズする(実装例)

//アイコンのイメージを指定する(今回は、postクラスのphoto(UIImage)を使用しました。)
let image = self.post.photo

Loaf("\(self.post.name)のポストを編集しました。", 
      //.customを指定し、背景色にsystemGreenとアイコンにimageを指定する
      state: .custom(.init(backgroundColor: .systemGreen, icon: image)), 
      location: .top, presentingDirection: .vertical, dismissingDirection: .vertical, sender: self).show()

実行結果↓
20200116_021515.GIF

引用(スペシャルサンクス)

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