- 投稿日:2020-05-27T23:22:44+09:00
【SwiftUI】基本的なモディファイアの種類と使い方③(画像編)
SwiftUIのレイアウトに画像を表示するために必要なImageビュー
システムアイコン
システムから取得した画像をアイコンを加工する時、テキストビューで用いるようなモディファイアが使用される
Image(systemName: "circle.grid.hex.fill") .font(.largeTitle) .foregroundColor(.orange)resizableモディファイア
画像をリサイズ
倍率を間違えると画像が縦や横に歪むことがあるImage("hoge_image") .resizable() .frame(width: 300.0 ,height: 120.0)scaledToFitモディファイア
縦横比を維持しながらビューサイズにあうようにリサイズ
Image("hoge_image") .resizable() .scaredToFit() .frame(width:300,height:120)scaledToFillモディファイア
縦横比を維持しながらいずれかの部分がビューサイズいっぱいになるように伸縮
ビューサイズからはみだすことがあるImage("hoge_image") .resizable() .scaredToFill() .frame(width:300,height:120)clippedモディファイアでビューサイズにクリップできる
- 投稿日:2020-05-27T17:45:27+09:00
Swiftの構造体はC++に渡せるよって話
! C++とSwift間で構造体を渡すことは保証されておらず、メモリレイアウトが異なる場合があるそうです。
この記事は参考程度に
背景
Swiftは比較的高速な言語ですが、やっぱり速度面でC++を使いたくなることはあります。
しかし、Swiftが直接サポートしているのはCからのブリッジのみです。なので、
C++の構造体をObjCでラップして...
なんて形で渡すことが多いです。でもこれだとかなりオーバヘッドが大きくて(特にObjCの動的ディスパッチあたりが)、結局C++を使った意味は...?となることが多いです。
なので、実はSwiftの構造体くらいならC++に直接渡せるよって話です。
実験
Swiftでこんな構造体を作ります。
struct Point { var x: Double var y: Double } struct Size { var width: Double var height: Double } struct Rect { var origin: Point var size: Size }それをポインタにして、
cppcall
に投げます。var frame = Rect(origin: Point(x: 100, y: 200), size: Size(width: 300, height: 400)) withUnsafeMutablePointer(to: &frame) {ptr in cppcall(UnsafeMutableRawPointer(ptr)) }次にC++側です。
Swiftで定義した構造体と同じ構造を持つ構造体をC++側で定義します。
struct Point { double x; double y; }; struct Size { double width; double height; }; struct Rect { Point origin; Size size; }; void cppcall(void *swiftStruct) { auto casted = (struct Rect*)(swiftStruct); std::cout << casted->origin.x << std::endl; std::cout << casted->size.height << std::endl; }Headerで以下のようにextern "C"します。受け取るのは
void*
型です。extern "C" { void cppcall(void* swiftStruct); }これで、実行するとそのままC++にSwiftでインスタンス化した構造体がC++で使えます。
解説のようなもの
これがうまく行くのはSwiftとC++でメモリ上の構造体の扱いが同じだからです。どちらもメンバーを最初から順番にメモリに並べているだけです。(SwiftはOptionalな構造体ではちょっと扱いが変わりますが...)
- 投稿日:2020-05-27T17:40:12+09:00
iOSアプリにYouTube動画を埋めこもう2020
iOSアプリにYouTube動画を埋めこんだので、その覚書です。
基本
YouTubeの公式ドキュメントを見ると、YouTube-Player-iOS-HelperというOSSを使うのを推奨されます。
基本WebViewをベースに、いい感じのサイズにしたYouTube動画を埋めこんで、コントローラーをつけるという感じです。(公式ドキュメント)
Embed YouTube Videos in iOS Applications with the YouTube Helper Library(わかりやすいチュートリアル)
[Swift] YouTube-Player-iOS-Helper を使って YouTube 動画を再生してみる2020年に直面した問題
YouTube-Player-iOS-Helperなんですが、メンテナンスされていないっぽいです。
最近AppleからUIWebView完全廃止の連絡があって、WKWebViewに移行したと思うんですが、
YouTube-Player-iOS-HelperはUIWebViewを使っているので、これ使っているとReject理由になります。
(YouTube公式に対応して欲しいですが、その辺はGoogleとAppleなので、あんまり息があってない感じはします)幸いOSSなので、有志の方がWKWebView移行したライブラリを作ってくれていて、YoutubePlayer-in-WKWebViewがあるので、こちらを使いました。
CocoaPodpod "YoutubePlayer-in-WKWebView", "~> 0.3.0"サムネイル画像が自由に調整できない問題
で、ここから実装にあたって苦労したこと。
サムネ画像をバーンってやっぱ表示したかったんですけど、
最初200*200ぐらいでつくろうとしたら、トリミングされた形で表示されました。
YouTubeのサムネイル画像は、下記のサイズで提供されるみたいです。
- 高クオリティ(480x360) ※ width x height
- 中クオリティ(320x180)
- 標準クオリティ(120x90)
- HQ動画の標準クオリティ(640x480)
- FULLHDのクオリティ(1920x1080)
基本iPhoneのサイズを意識すると、中クオリティ or 標準クオリティが多いでしょうか。
(iPadが入ってくると、それ以上のサイズもあるかと思われます)
このサイズに合っていないと、それより一つ大きいサイズのサムネをとってきて、よしなにトリミングするっぽいです。
YouTube-Player-iOS-Helper(から派生したYoutubePlayer-in-WKWebView)だと、
たとえば400x200のViewに中クオリティ(320x180)を引き伸ばして表示、みたいなことはできない模様です。パラメーターが効かない問題
load(withPlaylistId:playerVars:)
のplayerVarsでプレイヤーを制御するパラメーターが色々指定できるんですが、
実際やってみると想定どおり調整できませんでした。まず
autoplay
は効かなかったです。
アプリ側のライフサイクルとかもあんのかな? と思ってますが、ちょっと謎です。
iv_load_policy
(動画アノテーション)も効かなかったですね。
(動画アノテーション:動画の上に出るクリック可能な文字)
playsinline
/controls
は効きました。
modestbranding
(YouTubeロゴ非表示)は効かなかったんですが、
メソッドをload(withPlaylistId:)
にするとなぜかYouTubeロゴが非表示になりました。この辺の挙動はOSS内部の問題なのか、API側の問題なのか謎です。
- 投稿日:2020-05-27T17:35:12+09:00
Xcode11で設定にバージョン・ビルド番号・ライセンスを表記する
Xcode11で設定にバージョン・ビルド番号・ライセンスの表記の仕方を書いていきたいと思います。
Xcode11からVersionとBuildのスクリプトが変わったらしいです。
License表記用のライブラリとしてLicensePlistを使用します。
(すでに、表記するサンプルとしてPKHUDを入れてあります。)注意
・必ずバージョン番号とビルド番号をメモしておいてください。
[ライセンスの表示]
pod 'LicensePlist'
を追加して、ライブラリをアップデート(pod update
)します。
.xcworkspace
ファイルを開いて、以下の画像の手順で新しいRun Scriptを作成します。
作成できたら、Run Scriptとなっている名前を変更しましょう。(別ファイルを作成した時にわかりやすくするため)
ここでは、『License Plist(Run Script)』としています。
ここまでできたら、以下のスクリプトをそのままコピペしましょう。
if [ $CONFIGURATION = "Debug" ]; then ${PODS_ROOT}/LicensePlist/license-plist --output-path $PRODUCT_NAME/Settings.bundle --github-token YOUR_GITHUB_TOKEN fiここまでで、一旦ビルドします。
問題なければ、そのままアプリが起動するはずです。Root.plistを作成
ここまでできたら、ファイル名は
Root.plist
で作成します。
Root.plistを編集
作成できたら、Root.plistをSouceCodeで表示。
表示できたら、以下のコードをコピペ。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>PreferenceSpecifiers</key> <array> <dict> <key>Type</key> <string>PSGroupSpecifier</string> <key>FooterText</key> <string>Copyright</string> <!--コピーライトを追加したい方はCopyrightの中身を変更することでできます--> </dict> <dict> <key>Type</key> <string>PSTitleValueSpecifier</string> <key>DefaultValue</key> <string>1.0.0</string> <key>Title</key> <string>Version</string> <key>Key</key> <string>sbVersion</string> </dict> <dict> <key>Type</key> <string>PSTitleValueSpecifier</string> <key>DefaultValue</key> <string>1</string> <key>Title</key> <string>Build</string> <key>Key</key> <string>sbBuild</string> </dict> <dict> <key>Type</key> <string>PSChildPaneSpecifier</string> <key>Title</key> <string>Licenses</string> <key>File</key> <string>com.mono0926.LicensePlist</string> </dict> </array> <key>StringsTable</key> <string>Root</string> </dict> </plist>Setting.bundleを追加
プロジェクトファイルの直下に生成された
Settings.bandle
をXcodeドラッグ&ドロップして読み込みます。
さっき編集した
Root.plist
はSetting.bundle
のなかに移動させましょう。
[バージョン・ビルド番号を表示]
新規Run Scriptを作成
先ほどRun Scriptをを作成した時と同じ要領で、もう一つ新しいRun Scriptを作成します。
作成したら、以下のスクリプトをコピペします。
APP_VERSION="$MARKETING_VERSION" /usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:1:DefaultValue ${APP_VERSION}" "${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}/Settings.bundle/Root.plist" BUILD_NUMBER="$CURRENT_PROJECT_VERSION" /usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:2:DefaultValue ${BUILD_NUMBER}" "${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}/Settings.bundle/Root.plist"
info.plist
を確認
info.plist
を先ほどと同じSource Codeで表示して、以下の項目のみ書き換えます。<key>CFBundleShortVersionString</key> <string>$(MARKETING_VERSION)</string> <key>CFBundleVersion</key> <string>$(CURRENT_PROJECT_VERSION)</string>プロジェクトのVersionとBuildを確認
VersionとBuildが空になっていたら、元の値を入力してあげましょう。
いざビルド
では、ビルドして設定をみてみましょう!
このように表示できました!
参考にさせていただいた記事
iOS13の設定アプリにつまずいた - Qiita
Xcode11でのsetting.bundleのバージョン更新スクリプト - Qiita
[iOS][Swift] 設定画面に【アプリのバージョン番号】【CocoaPods導入したライブラリのライセンス】を表示する - Qiita
[iOS] アプリの設定画面にバージョン表記と謝辞を自動で設定する - Developers.io
CocoaPodsでLicensePlistを使う - 野生のプログラマZ
- 投稿日:2020-05-27T15:30:14+09:00
俺の嫌いなXcodeが5秒でイチオシ開発環境に
iOS開発者の皆さん、毎日、ハッピーな開発ライフ過ごしてますでしょうか。
楽しい開発には快適な開発環境は欠かせませんが、もちろんiOS開発者の皆さんが使うのはXcodeですよね。
いや、俺はAppCodeという人はそっとタブを閉じましょう。JetBrains製品、良いですよね。Android Studio も素晴らしい。どんな言語でも同じ様な操作感で。でもちょっともっさりしてるんだよな。
その点、Xcodeは動きはキビキビしていて玄人プログラマー好み。でも一つ、すごく嫌なところがありました。これさえ直してくれれば最高なのに。
普通、Xcodeって編集する時、複数のタブ開くじゃないですか。自分の場合はこんな感じ。
Storyboardと関連のソースファイルを幾つか。そして、デバッグする時は、気になってるところにブレークポイント張ります。プログラム実行して、さて、気になるところに差し掛かると...
あーこのタブでデバッグ状態になって違うファイル開かれたわー、確かに昔そこにブレークポイント張っとったわー。というか、そもそもなんのファイル見てたかも不明だわー。
ということが、多々ありました。(該当のタブで戻るボタン押せば戻るんですが)
ここが本当に嫌いだった。なんでAppleはこれで平気なの?
平気じゃなかった様です。全然普通に回避できました。いつからだろう。
Xcodeの「Preference ー Behaviors」または「Edit Behaviors」を開きましょう。
RunningのPausesが、ブレークポイントで一時停止した時の設定の様です。
「Show tab named」のところをチェックして名称を入れましょう。
おー、さっきつけた名前で自動でタブが生成されて、編集してたタブはそのままです。
ちなみに、「Play sound」のところを設定すると、一時停止したときにサウンドがなります。同じ様に「Speak announcement using」を設定すると、映画に出てくるハッカーのコンピュータみたいでオススメです。
これ知らなかったでしょ?いや知ってたって!?もしかして、知らなかったの自分だけ??しかしなんで気づかなかったんだろう。
それでは素敵な開発ライフを!
参考
XcodeのBehaviorsを設定してデバッグ時にウインドウを自動で切り替える
https://techracho.bpsinc.jp/wingdoor/2019_12_04/83304
筆者参考
札幌圏でリモート開発、在宅勤務を中心としたシステム開発の会社を経営しています。
- 投稿日:2020-05-27T14:44:43+09:00
SwiftのReverse GenericsというかOpaque Typeの紹介(社内勉強会用)
Swiftとは
Wikipedia より
Swift(スウィフト)は、アップルのiOSおよびmacOS、Linuxで利用出来るプログラミング言語。Worldwide Developers Conference (WWDC) 2014で発表された。アップル製OS上で動作するアプリケーションの開発に従来から用いられていたObjective-CやObjective-C++、C言語と共存することが意図されている。
swift.org Language Guide より
Swift is a type-safe language, which means the language helps you to be clear about the types of values your code can work with.
Type-safe
var id: Int = 123 var name: String = "abc" name = 456 // Compile error: cannot assign value of type 'Int' to type 'String var ids: Array<Int> = [] ids.append(id) ids.append("xyz") // Compile安全に、呼び出し側が指定した型で、Arrayを使用することができる (Generics)
Genericsを使わないと、
class IntStack { var items = [Int]() func push(_ item: Int) { items.append(item) } func pop() -> Int { return items.removeLast() } } var ids = IntStack() ids.push(id) ids.push(name) // Compile error: cannot convert value of type 'String' to expected argument type 'Int'
StringStack
など、利用する型ごとにクラスが必要?
Genericsを使うと
class Stack<Element> { var items = [Element]() func push(_ item: Element) { items.append(item) } func pop() -> Element { return items.removeLast() } } var ids = Stack<Int>() ids.push(id) ids.push(name) // Compile error: cannot convert value of type 'String' to expected argument type 'Int' var names = Stack<String>() names.push(name)
でも逆に、呼び出し元が決めたい場合も
// これを公開し、実際のstructは隠蔽する protocol ChatRoom { var id: Int { get } var name: String { get } } // DM: チャットルーム名は参加者名のカンマ区切り、 fileprivate struct DMChat: ChatRoom { var id: Int var name: String { memberNames.joined(separator: ", ") } var memberNames: Array<String> } // チーム: 名前やアバターアイコンを持つ fileprivate struct TeamChat: ChatRoom { var id: Int var name: String var memberIDs: Array<Int> var avatarIcon: String } func loadDMChat(id: Int) -> ChatRoom { return DMChat(id: id, memberNames: ["Taro"]) } func loadTeamChat(id: Int) -> ChatRoom { return TeamChat(id: id, name: "ACCESS", memberIDs: [100,101,102], avatarIcon: "file") }
loadDMChat()
ではChatRoom
として内部の型は隠蔽したいんだけど、実際に返るのは常にDMChat
。これは擬似コードfunc loadDMChat(id: Int) -> <C: ChatRoom> C { return DMChat(id: id, memberNames: ["Taro"]) }こんな感じに、Reverse Genericsしたい。
Returning an Opaque Type
-func loadDMChat(id: Int) -> ChatRoom { +func loadDMChat(id: Int) -> some ChatRoom { return DMChat(id: id, memberNames: ["Taro"])こう書くことで、コンパイル時にこの戻り値は
DMChat
型とみなされる。// Compile error: cannot convert value of type 'some ChatRoom' to specified type 'DMChat' private let dm: DMChat = loadDMChat(id: 1)仮に
DMChat
型が呼び出し元に見えていたとしても、その型で受け取ることを許可しているわけではない。
何が嬉しいのか
- 型の隠蔽
- オーバーヘッドがない
- どんなオーバーヘッド?
Value type
var name: String = "abc" print(name) // abc var name2 = name name.append("1") print(name) // abc1 print(name2) // abc
- 実は、SwiftのStringはStructで、値型
- 値渡し、つまり、メモリの確保、コピーなどが行われれる
- 実際には、Copy-On-Writeなど最適化されていはいる
https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html
In fact, all of the basic types in Swift—integers, floating-point numbers, Booleans, strings, arrays and dictionaries—are value types, and are implemented as structures behind the scenes.参照型のシャローコピーにより引き起こされる問題(コピー元も変更されてしまう)の解決として、値の変更の容易さや、イミュータブルクラスを都度まるごと作り直すオーバーヘッドを考え、値型がよいという考え方らしい(?)
終わり
参考URL
- 投稿日:2020-05-27T13:29:26+09:00
FirebaseCrashlyticsで強制クラッシュさせる時の方法が変わっていたお話
環境
- Xcode 11.4
- Swift 5.2
- FirebaseCrashlytics 4.0.0-beta.5
概要(2020/05/27時点)
FirebaseCrashlyticsで強制クラッシュをさせようとしたら、以下の二点で微妙に詰まったのでメモ書きとして残しています。
- シングルトンインスタンス取得の方法が変わっていた
- 強制クラッシュを起こすメソッドが消えていたシングルトンインスタンス取得
以前のver
Crashlytics.sharedInstance()現在のver
Crashlytics.crashlytics()クラッシュさせる
以前のver
Crashlytics.sharedInstance().crash()現在のver
公式ドキュメントによると
fatalError()
を使えとのこと。fatalError()引っかかった原因
日本語のドキュメントのみ更新されていませんでした...
- 投稿日:2020-05-27T13:20:22+09:00
なぜか画面がブラックアウトしたとき
メモ
アプリ作ってたらあるタイミングで急に画面が完全にブラックアウトして何も表示されなくなった。
プロジェクト丸ごと昇天したのかと思ったがそんなことはなかった。
SceneDelegateで予め宣言されているwindow変数を使わずに新しくwindow変数を定義して使っていたせいらしい。
(何が違うんだ、、?)
- 投稿日:2020-05-27T08:29:01+09:00
PageViewController(UIViewController)とPageControl
ツイートのコード公開用。
コードの説明は割愛。
・UIViewControllerを4つ用意
・1つはPageViewControllerDelegate、Datasourceに準拠
(サブクラスはUIViewController)
・残り3つはそれぞれidentifierをつけておく。(表示画面用)
(FirstViewController、SecondViewController、ThirdViewController)あとは残り3つのUIViewControllerを好きなように
いじる事で好きなUIに設定。import UIKit class PagesViewController: UIViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource { var pageViewController:UIPageViewController! let pageControl = UIPageControl() override func viewDidLoad() { super.viewDidLoad() pageViewController = UIPageViewController(transitionStyle:.scroll, navigationOrientation: .horizontal, options: nil) pageViewController.dataSource = self pageViewController.delegate = self pageViewController.setViewControllers([getFirst()], direction: .forward, animated: true, completion: nil) pageViewController.view.frame = view.frame view.addSubview(pageViewController.view!) let position = UIScreen.main.bounds.size pageControl.frame = CGRect(x: position.width / 2 - 19.5, y: position.height - 100, width: 39, height: 37) pageControl.numberOfPages = 3 pageControl.currentPage = 0 pageControl.isUserInteractionEnabled = false pageControl.pageIndicatorTintColor = .black pageControl.currentPageIndicatorTintColor = .green view.addSubview(pageControl) } func getFirst() -> FirstViewController { return storyboard!.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController } func getSecond() -> SecondViewController { return storyboard!.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController } func getThird() -> ThirdViewController { return storyboard!.instantiateViewController(withIdentifier: "ThirdViewController") as! ThirdViewController } //右スワイプ時に呼ばれる func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { if viewController.isKind(of: SecondViewController.self) { return getThird() } else if viewController.isKind(of: FirstViewController.self) { return getSecond() } else { return nil } } //左スワイプ時に呼ばれる func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { if viewController.isKind(of:SecondViewController.self) { return getFirst() } else if viewController.isKind(of:ThirdViewController.self) { return getSecond() } else { return nil } } /*スワイプに伴う処理(pagecontrolのcurrentPageの indexのインクリメント,デクリメントなど)はここに書く*/ func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating: Bool, previousViewControllers: [UIViewController], transitionCompleted: Bool) { if transitionCompleted { if let currentVC = pageViewController.viewControllers?[0] { let vcName = String(describing: type(of:currentVC)) if vcName == "FirstViewController" { self.pageControl.currentPage = 0 } else if vcName == "SecondViewController" { self.pageControl.currentPage = 1 } else if vcName == "ThirdViewController" { self.pageControl.currentPage = 2 } else { print("クラスの取得に失敗しました。") } } } } }
- 投稿日:2020-05-27T07:08:46+09:00
Qiita の記事の閲覧数とLGTMの数を表示するiOSアプリを作成
この記事の内容について
Open-Source Source code / オープンソース: https://github.com/mszmagic/Qiita-Contribution-Counter
この記事では、いくつかのヒントや私がアプリで使用した技術について説明しています。どちらかというとケーススタディのようなものです。
- ローカルデバイス上でユーザーのトークンを保存する
- 既存のウェブクッキーを使用してモバイルアプリケーションにユーザーをログインさせる
URLSession
を使用してGET requests
を作る- Swift でJSONレスポンスを解析する
- リモートサーバーから画像をロードする
もちろん、ここで私はすでにあるものを再発明しませんでした、いくつかの既存のオープンソースフレームワークを使用しました。記事の次の部分でそれについて紹介します。
概要
Githubと同じように
Qiita
コントリビューションを表示したかったのです。そこで次のことができるオープンソースiOSアプリを作りました。
1.各記事の合計読み取り回数と読み取り回数を提供する
2.各記事の合計 LGTMs と views を提供する
3.Githubに似たコントリビューションブロックを表示して、当月内に記事を公開した日を表示する
コード構造
View Controller
ここで、ユーザーは
Qiita APIページに進む
をクリックして、新しいAPIトークンを作成できます。Safariのクッキーを自動的に使用することにご注意ください。
ASWebAuthenticationSession
ユーザーの既存のブラウザーセッションのクッキーをここで利用できるよう、
ASWebAuthenticationSession
を用います。そうすればユーザーは再度ログインしなくてもよくなります。guard let authURL = URL(string: "https://qiita.com/settings/tokens/new") else { return } let session = ASWebAuthenticationSession(url: authURL, callbackURLScheme: "") { callbackURL, error in // Handle the callback. } session.presentationContextProvider = self session.start()また、
presentationContextProvider
をセットアップして、ASWebAuthenticationSession
がビューを表示する場所を認識できるようにする必要があります。/* ASWebAuthenticationSession がどこにビューを表示すべきか判断するためです */ extension ViewController: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { return view.window! } }ユーザーのトークンをキーチェーンに保存して、次回ユーザーがトークン値を再度入力する必要がないようにします
通常、アプリは機密情報(トークンなど)をキーチェーンに保存します。キーチェーンの使用に関するAppleの公式ドキュメントは以下のとおりです:https://developer.apple.com/documentation/security/keychain_services
キーチェーンサービスを使用するにはある程度のコードが必要なので、私はGithub上のオープンソースのキーチェーンヘルパーを使用しました。
https://github.com/evgenyneu/keychain-swift
let keychain = KeychainSwift() keychain.set(tokenTextField.text ?? "", forKey: "qiitaToken") tokenTextField.text = keychain.get("qiitaToken") ?? ""userTableView
リモート画像フェッチ
ここでは、
Kingfisher
というオープンソースのフレームワークを使用しました。https://github.com/onevcat/Kingfisher
@IBOutlet weak var profileImageView: UIImageView! //画像をダウンロードして読み込みます if let imagePath = profileImagePath, let convertedURL = URL(string: imagePath) { DispatchQueue.main.async { self.profileImageView.kf.setImage(with: convertedURL) } }self.profileImageView.kf.setImage(with: convertedURL)requestHelper.swift
ここでは、
URLSession
を使ってリクエストしています:let sessionConfig = URLSessionConfiguration.default let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil) guard let URL = URL(string: "https://qiita.com/api/v2/authenticated_user/items") else { delegate?.onTaskFailed(reason: "URL convertion failed!") return } var request = URLRequest(url: URL) request.httpMethod = "GET" // Headers request.addValue("Bearer \(userID ?? "")", forHTTPHeaderField: "Authorization") /* Start a new Task */ let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in if let httpResponse = response as? HTTPURLResponse { let statusCode = httpResponse.statusCode if error == nil && statusCode >= 200 && statusCode < 400 { //成功 let allItems = try! JSON(data: data!).array for item in allItems ?? [] { if let id = (item.dictionary?["id"])?.stringValue { self.fetchIndividualArticle(id: id) } } return } } // 失敗 self.delegate?.onTaskFailed(reason: error?.localizedDescription ?? "Unknown error. Please check your token and try again.") }) task.resume() session.finishTasksAndInvalidate()受信結果を解析:
if let fetchedData = data { if let parsedData = try? JSON(data: fetchedData).dictionary { //ユーザー名 let name = parsedData["name"]?.stringValue //説明 description let description = parsedData["description"]?.stringValue //プロフィール画像のURLパス let profileImage = parsedData["profile_image_url"]?.stringValue // completionHandler(profileImage, name, description, nil) } }ここでは、
SwiftyJSON
というオープンソースのフレームワークを使用しました。https://github.com/SwiftyJSON/SwiftyJSON
Githubと同じように
Qiita
コントリビューションを表示したかったのです。ここでは、
LSHContributionView
というオープンソースのフレームワークを使用しました。
- 投稿日:2020-05-27T07:05:09+09:00
[SwiftUI] ListでOffsetを取得する
SwiftUIのListは、UIKitのTableViewよりも簡単にリストを構築できるようになって嬉しいです。
しかし、offsetが今までのように簡単に取得できなかったので、カスタムListを作ってみました。使い方
struct ContentView: View { @State private var offset: CGFloat = 0 var body: some View { TrackableList(contentOffset: $offset) { ForEach(0..<100) { _ in Text("offset:\(self.offset)") } } } }Offset取得可能なカスタムListを作る
struct TrackableList<Content>: View where Content: View { @Binding var contentOffset: CGFloat let content: Content init(contentOffset: Binding<CGFloat>, @ViewBuilder content: () -> Content) { self._contentOffset = contentOffset self.content = content() } var body: some View { GeometryReader { outsideProxy in List { ZStack { GeometryReader { insideProxy in Color.clear .preference(key: ScrollOffsetPreferenceKey.self, value: [outsideProxy.frame(in: .global).minY - insideProxy.frame(in: .global).minY]) // Send value to the parent } VStack { self.content } } } .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in self.contentOffset = value[0] } // Get the value then assign to offset binding } } } struct ScrollOffsetPreferenceKey: PreferenceKey { typealias Value = [CGFloat] static var defaultValue: [CGFloat] = [0] static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) { value.append(contentsOf: nextValue()) } }Viewの外側と内側にそれぞれ
GeometryReader
を置き、その差し分を計算することでoffsetを取得しています。参考
こちらの記事を参考にしました。
以下記事では、Listではなく、ScrollViewのoffsetを取得しています。
horizontal
、vertical
やshowIndicators
などの設定もできるようになっています。
https://medium.com/@maxnatchanon/swiftui-how-to-get-content-offset-from-scrollview-5ce1f84603ec
- 投稿日:2020-05-27T07:05:09+09:00
【SwiftUI】 ListでOffsetを取得する
SwiftUIのListは、UIKitのTableViewよりも簡単にリストを構築できるようになって嬉しいです。
しかし、offsetが今までのように簡単に取得できなかったので、カスタムListを作ってみました。使い方
struct ContentView: View { @State private var offset: CGFloat = 0 var body: some View { TrackableList(contentOffset: $offset) { ForEach(0..<100) { _ in Text("offset:\(self.offset)") } } } }Offset取得可能なカスタムListを作る
struct TrackableList<Content>: View where Content: View { @Binding var contentOffset: CGFloat let content: Content init(contentOffset: Binding<CGFloat>, @ViewBuilder content: () -> Content) { self._contentOffset = contentOffset self.content = content() } var body: some View { GeometryReader { outsideProxy in List { ZStack { GeometryReader { insideProxy in Color.clear .preference(key: ScrollOffsetPreferenceKey.self, value: [outsideProxy.frame(in: .global).minY - insideProxy.frame(in: .global).minY]) // Send value to the parent } VStack { self.content } } } .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in self.contentOffset = value[0] } // Get the value then assign to offset binding } } } struct ScrollOffsetPreferenceKey: PreferenceKey { typealias Value = [CGFloat] static var defaultValue: [CGFloat] = [0] static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) { value.append(contentsOf: nextValue()) } }Viewの外側と内側にそれぞれ
GeometryReader
を置き、その差し分を計算することでoffsetを取得しています。参考
こちらの記事を参考にしました。
以下記事では、Listではなく、ScrollViewのoffsetを取得しています。
horizontal
、vertical
やshowIndicators
などの設定もできるようになっています。
https://medium.com/@maxnatchanon/swiftui-how-to-get-content-offset-from-scrollview-5ce1f84603ec
- 投稿日:2020-05-27T02:33:37+09:00
API GatewayでCognitoの未認証ユーザへアクセスを許可する
やりたいこと
Cognitoユーザプールの認証済みユーザ、未認証ユーザだけが呼び出せるAPI Gatewayを作成する。
APIの直接呼び出しは許可しない。認証プロバイダは使用しないけど、特定のアプリのゲストアクセスも使用できるAPIを作成します。
前提
Xcode:11.4
Amplifyが使用可能
AWSMobileClient:2.13.0
AWSAPIGateway:2.13.0AWS構成図
Cognitoユーザプールの作成
Amplifyを使用してユーザを作成します。
Xcodeプロジェクトディレクトリでコンソールからamplifyを使ってCognitoユーザプールを作成。
amplify add auth内容は任意で作成して
amplify pushとしてCognitoのユーザ作成を行います。
API Gatewayの設定
1.RESTでAPIを作成。Lambdaファンクションの実装はご自由に。
2.コンソールで作成したAPIを選択。
3.メニューから「リソース」を選択し、メソッド(GETなど)を選択。
4.メソッドリクエストの「認可」を"AWS_IAM"を選択。
5.デプロイしてSDKも生成しておく。ロールにポリシーを設定
1.Cogniteのコンソールに移る。
2.Amplifyで生成したプールIDを選択。
3.フェデレーティッドアイデンティティの画面から右上にあるIDプールの編集を選択。
4.認証されていないIDセクションから「認証されていない ID に対してアクセスを有効にする」をチェック
5.同じ画面で認証されていないロール、されているロールが表示されているので覚えておく。ロールにポリシーを追加
1.IAMに移動
2.Cognitoの認証、未認証のロールを選択しポリシーをアタッチする。
以下のようなポリシーでAPI Gatewayの呼び出しを許可する。{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "execute-api:*" ], "Resource": [使いたいAPI GatewayのARN名] } ] }Xcode側の実装
結構ハマりどころ。
API Gatewayで生成したSDKをプロジェクトにインポート。BridgingHeaderも。
podでAWSMobileClientとAWSAPIGatewayをインストールする。バージョンは2.13.0を使用した。Podfile$awsVersion = '~> 2.13.0' pod 'AWSMobileClient', $awsVersion pod 'AWSAPIGateway', $awsVersionAppDelegateに以下を追加。
AppDelegate.swiftimport AWSMobileClient func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // AWSMobileClientを初期化 AWSMobileClient.sharedInstance().initialize { (userState, error) in guard error == nil else { print("Error initializing AWSMobileClient. Error: \(error!.localizedDescription)") return } print("AWSMobileClient initialized.") } return true }APIを呼び出す処理はこんな感じ。
import AWSAPIGateway import AWSCognitoIdentityProvider func testGetFunc(){ let credentialsProvider = AWSCognitoCredentialsProvider(regionType:.リージョンのenum, identityPoolId:"プールID") let configuration = AWSServiceConfiguration(region:.リージョンのenum, credentialsProvider:credentialsProvider) AWSServiceManager.default().defaultServiceConfiguration = configuration let client = [API Gatewayで生成したSDKクラス].init(configuration: AWSServiceManager.default().defaultServiceConfiguration) client.rootGet().continueWith { (task) -> Any? in if let error = task.error { print("Error occurred: \(error)") // エラー時の処理 return nil } // 正常時の処理 if let result = task.result { // task.resultにはSDKクラスになっているので好きなように処理する。 result.hogeList?.forEach({ (item) in print(item.hogeValue) }) } return task } }だいぶ端折りましたがこんな感じです。
確認する
ブラウザから、他のアプリからのアクセスがNG。
今回作成したアプリからのアクセスは正常に戻り値が取得可能になっていることを確認します。ハマったところ
- API Gatewayで生成したSDKクラスが全然動かなかった。
- AWSMobileClient、AWSAPIGatewayは最新版はAPIが変わっているので使い方がわからず。2.13.0にとどめた
- API GatewayのオーソライザーでCognitoを使うのかと思っていたが違っていた。
- 投稿日:2020-05-27T00:28:11+09:00
"Could not find a storyboard named ‘Main’ in bundle NSBundle"に遭遇した時
環境
- macOS Catalina 10.15.4
- Xcode11.4
- TargetSDK iOS13.4
いつ遭遇したか
iOSアプリを作る際に、最初に表示したい画面の名前を変えたかったので、
Main.storyboard
のファイル名をSample.storyboard
に変えてビルドすると、画面が描画されずにCould not find a storyboard named ‘Main’ in bundle NSBundleと表示されてクラッシュした。
Could not find a storyboard named 'Main' in bundle NSBundle 対処方法を参考に、info,plistの
Main storyboard file base name
を消したり、Sample
に変えても効果なし。解決方法
Xcode全体に
Main
で検索をかけてみると、info.plist内にもう一箇所Storyboardの名前を指定している箇所を見つけた。
Application Scene Manifest
↓
Scene Configuration
↓
Application Session Role
↓
Item 0 (Default Configuration)
↓
Storyboard Nameこれが
Main
になっていたので、Sample
に変更すると、無事にビルドできた。
ファイル名変えるだけなのにこんなトラップがあるとは...