- 投稿日:2020-01-16T22:09:14+09:00
【swift5】ストップウォッチタイマーの作り方
キャプチャ
ソース全部
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にも優良な情報があってプログラミングを学びたい人には天国ですね。
(変な情報もたくさんあるのでお気をつけを。)
- 投稿日:2020-01-16T21:55:06+09:00
【swift】ユーザーが任意のテーマカラーを設定できる機能
前置き
参考ページのまとめ集。
環境
xcode 11.3
swift 5.1.3
CocoaPods 1.8.4機能要件
テーマカラー選択機能
テーマカラー反映機能仕様
- テーマカラー設定ボタンを押すと設定ページへ
- 設定ページには、色が並んでいる
- 色をクリックすると、設定ページの背景色が変わる
- 決定ボタンを押すと、選択された色がテーマカラーとなる
- もとの画面に戻ったのち、テーマカラーが反映されている
設計
storyboard上に要素の追加
- コントローラー
- カラーパレット
- 決定ボタン
カラーパレットの設置
以下の記事に従ってcollectionViewを作成
qiita : 【Swift・iOS】XcodeのCollectionViewの使い方。タイル型(カード型)のレイアウト方法を解説
以下の記事を参考に、タップアクションを追加
以下の記事に従って形を整形
選択カラーの保存
userDefaultsの設定
テーマカラー格納用にクラスを作っておく
UIColorの変換
userDefaultsにカラーを保存できるように、UIColorをData型に変換する
- 投稿日:2020-01-16T19:24:06+09:00
VaporからDockerで立てたMySQL8に接続し、Leafで表示する
前回の記事 VaporからDockerで立てたMySQL8に接続する では Vapor から Docker に立てた MySQL に接続するところまで実装しました。
今回の記事では、接続したデータベースから値を取り出し、テンプレートエンジンである Leaf で使っていこうと思います。リポジトリはこちら O-Junpei/sommelier-vapor
まず Docker で立てた MySQL にテーブルを作成し、検証用のデータを追加します。
IT企業の技術ブログをまとめを作りたいので、企業ブログの記事の情報が入るarticle
テーブルを作成します。01_ddl.sqlcreate 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.sqlSET 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.sql
と02_data.sql
をinitdb
ディレクトリに入れ、initdb
ディレクトリ は/docker-entrypoint-initdb.d
ディレクトリ としてマウントするようにしました。
こうすることで、MySQL コンテナ作成時に2つの.sql
ファイルがコンテナの新規作成時に実行されます。docker-compose.ymlservices: 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.swiftimport 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.swiftimport 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 のデータを簡単に扱うことができます。
ArticleController.swiftimport 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.swiftimport 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")
- 投稿日:2020-01-16T18:22:21+09:00
【初心者向け】Showing All Issues "アプリ名" requires a provisioning profile. Select a provisioning profile in the Signing & Capabilities editor.が出た時の対処法。
- 投稿日:2020-01-16T18:21:08+09:00
【初心者向け】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です。
- 投稿日:2020-01-16T17:32:57+09:00
【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.swiftimport 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とはなんぞや、とかどうやって使うの、とかは以下の記事が参考になりました。
- 投稿日:2020-01-16T17:32:53+09:00
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-16T16:35:07+09:00
Swift: map()でゴチャついた処理をreduce(into:)でスッキリさせる
はじめに
reduce(into:)
はちょっと理解あまりしていなく使用を避けていた節があり、map系
でゴリゴリしていたらこれ使えるよってアドバイスもらって使い方がわかったので備忘録。reduce(into:)の用途
official
https://developer.apple.com/documentation/swift/array/3126956-reduce
想定
APIレスポンスを元に
TableView
にて、セクションを切ってアイテムをリスト表示させたいケースがありました。Header
SectionHeader
は別途使用していたのと、viewをいじいじしたCustomView
を使用したかったなどで、header
はCustomCell
を使用したかった。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"),...]), ...]これをこうしたい。
なんならアイテムが無ければこうもしたい。
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
を返すcompactMap
でnil
掃除- 一時的に作成した配列と、最終的に使用したい配列の型が異なるので
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レスポンスを操作するときなどマッピングする機会はたくさんあると思うので使い所はありそうですし、スッキリしていいですね。まとめ
マッピング系処理のネタが増えてよかった。
読みもそうだけど、実装できるか判断するまで慣れが必要そう。練習します。
参考
- 投稿日:2020-01-16T16:35:01+09:00
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 バージョンに切り替わってました
メモ:
Carthageが利用するSwiftバージョンが切り替わらない...誰か解る人いたら教えてください。
- 投稿日:2020-01-16T15:56:28+09:00
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
- ToDo リスト
- ToDoリストを作ってみよう! | FLY-WEST
- 作っていく過程が書いてあり、結構詳しく書いてあるので良いと思います。
- Udemy のおすすめのセクション(購入していれば)
delegate
いろんなアプローチの解説があるので自分に合うものを探しましょう!
- 投稿日:2020-01-16T13:01:36+09:00
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
となるので気をつけましょう。参考
- 投稿日:2020-01-16T12:23:50+09:00
【Swift】Chartsでオシャレなグラフを描画してみる( 棒グラフ編 )
はじめに
Chartsというライブラリはご存知でしょうか?
とても有名なライブラリで、iOSでグラフを描画しようと思ったことがある人なら聞いたことや使ったことがあると思います。Chartsはグラフ自体を非常に細かくカスタマイズすることができる素晴らしいライブラリです✨
今回は忘備録も兼ねてChartsを使って今時でオシャレな棒グラフを作ってみたいと思います。オシャレな棒グラフって?
普段実装ばかりでデザインのことには疎いので、
まずは今時でオシャレな棒グラフのデザインをDribbbleやBehanceで調査してみました。
その結果以下のような棒グラフが流行っているような感じがしました。
Figmaでパパッと作ったので数値などは適当です。
Y軸の値表示なし Y軸の値表示あり ポイントをまとめるとこんな感じになりました。
- X軸の縦線は表示しない
- バーの右上と左上は角丸
- グラフの平均を表す線があって、点線で表示
- Y軸の値を他のラベルなどに表示する場合はグラフに表示しない
4は以下のようなUIの場合、グラフにはY軸の値を表示しない代わりに、グラフをタップするとグラフの上に表示されているラベルにY軸の値が表示されるようなイメージです。
早速作っていきたいところですが、一点問題があります。
棒グラフの角丸はサポートされていない
現在、Chartsでは棒グラフの角丸はサポートされていません。
PRは存在しますが、色々問題があってマージされていない状態です。さすがにフォークして直すのは時間がかかりそうだったので、角丸は省略したものを作成します。
個人的には角丸がなくても結構それっぽくなるので、これはこれでありだと思っています。実際に作ってみる
それでは実際に作っていきます。
Chartsのインストール
ChartsはCocoaPodsとCarthageに対応しています。
自分はCarthageでインストールしました。詳しくは公式のCarthage Installを参考にしてください。BarChartViewを追加する
Chartsのグラフを描画する
ChartView
はInterface Builderに対応しているのでStoryboardから追加することができます。今回は棒グラフを描画したいので一枚
UIView
を用意して、Custom ClassにBarChartView
、ModuleにCharts
を設定します。あとはいつも通りViewControllerにOutlet接続すればコードでいじれるようになります。
import UIKit import Charts class ViewController: UIViewController { @IBOutlet weak var barChartView: BarChartView! // ... }グラフに表示するデータについて
Chartsでは表示するデータを
ChartData
に変換する必要があります。
変換したChartData
をChartView
のもつ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デフォルトの状態でこれを表示するとこんな感じになります。
デフォルト 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)※
LimitLine
はleftAxis
もしくはrightAxis
に対して追加するので、追加する対象のenable
がfalse
になっていると表示されないので注意してください。ここまでで以下のような見た目になりました。だいぶそれっぽくなったと思います。
デフォルト 設定後 棒グラフの色と値の表示の設定
最後に棒グラフの色とグラフの上に値が表示しないように設定します。
これらの設定はChartDataSet
に対して設定します。let dataSet = BarChartDataSet(entries: entries) dataSet.drawValuesEnabled = false dataSet.colors = [.systemBlue]以上で完成です。
最終的にこんな感じになりました。
デフォルト 完成形 さいごに
Chartsで今時でオシャレな棒グラフを作ってみましたがいかがだったでしょうか?
他にもグラフをアニメーションさせたり、ハイライトした時にマーカーを表示できたり、X軸のラベルの表示をカスタマイズできるなど、まだまだカスタマイズできるので気になった方はぜひ試してみてください。公式のサンプルで一通り使い方はマスターできると思います。また、今回作ったグラフはサンプルとしてQiitaChartsSampleにあげてあるので、よければ参考にしてください。
分かりにくい部分や間違いがあれば指摘していただけると嬉しいです?♂️
最後まで読んでいただきありがとうございました。
- 投稿日:2020-01-16T11:11:13+09:00
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
,protocolA
的優先度反而不是更高的。不過如果這樣寫,明確的指定我的變數就是 protocol
A
let a2: A = AClass() a2.a = "some text"Xcode 就會如期待的抱怨了:
環境
Xcode 11.3.1, Playground
參考
- https://docs.swift.org/swift-book/LanguageGuide/Protocols.html 的
Property Requirements
- 投稿日:2020-01-16T11:11:13+09:00
為什麼 { 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
,protocolA
的優先度反而不是更高的。不過如果這樣寫,明確的指定我的變數就是 protocol
A
let a2: A = AClass() a2.a = "some text"Xcode 就會如期待的抱怨了:
環境
Xcode 11.3.1, Playground
參考
- https://docs.swift.org/swift-book/LanguageGuide/Protocols.html 的
Property Requirements
- 投稿日:2020-01-16T02:59:25+09:00
(swift)Loafで、サクッとエラーメッセージを表示する
Loafって?
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()引用(スペシャルサンクス)