#はじめに
対象としない読者:ちゃんとswiftを勉強したい人、「正しく」swiftを書きたい人、スタイリッシュなUIのものを作りたい人
対象とする読者:「あ、うごいた」を体験したい人、私
##環境
もの | バージョン |
---|---|
Xcode | 12.3 |
mac | Catalina |
iPhone | iOS14 |
私
「初めましてswiftさん」と言う状態。
これからappチームの人たちと関わることになったのでお勉強してみましたレベル。
かの有名な 挫折しない iPhoneアプリ開発「超」入門 第8版 【Xcode 11 & iOS 13】 完全対応を買ってサンプルコードを打ってみました、レベル。
こちらはxcode11の本ですが、自身のiPhoneがiOS14でxcode12を使う必要があった(と思う)ので最終的にxcode12にしました。
作った物
天気のAPIとニュースのAPIから情報を取得するiOSアプリ
大好きな観光地のベトナム:ダナンの情報を取得する
イメージ
初期表示
画面の状態 | 初期表示 | 「ダナンの天気」ボタン押下 | 「ニュース」ボタン押下 |
---|---|---|---|
画面イメージ | |||
関係のあるモジュール | ContentView.swift | ContentView.swift OpenWeatherMap.swift | ContentView.swift NewsView.swift NewsAPI.swift |
セットアップ
xcodeを落としてくる。
最新(xcode12)がいいならmacのApp Storeから取得できます。
ライブラリはCocoaPodsで管理する。
導入方法はこちらを参考にさせていただきました。
iOSライブラリ管理ツール「CocoaPods」の使用方法
インストール先は自身の作業プロジェクト配下です。
私の場合は /Users/{小松菜}/Practice/arekoreVN
でした。
ベトナムが好きすぎて、これからもっといろいろ足したいのであれこれベトナム
にしました。
抽象的なのでシステムのネーミングとしてはよくないですね。
※セットアップの途中でエラーになったら適宜対応してください。
私はRuby関連でエラーになったので、メッセージをみながら都度対応しました。
基本的に足りていない物をインストールしたり、古い物をバージョンアップしたり、という作業が発生します。
キーワードでぐぐると結構出てくるので安心してエラーメッセージと向きあえました。
CocoaPodのインストールでpod install
コマンドを実施したと思います。
するとプロジェクト配下にワークスペース(.xcworkspaceファイル)ができていますね?
それをひらけたらコーディング開始です。
感動ですね。はじめの一歩です。
が、開けたらもう一度閉じてください。
そしてPodfile を以下のように修正して再度pod install
してください。
# Uncomment the next line to define a global platform for your project
platform :ios, '12.0'
use_frameworks!
install! 'cocoapods',
:warn_for_unused_master_specs_repo => false
target '自身のプロジェクト名' do
# Comment the next line if you don't want to use dynamic frameworks
# Pods for 自身のプロジェクト名
pod "SwiftyJSON"
end
これで準備は完了です。(多分)
初期表示画面を実装する
早速画面を作っていきます。
この画面ですね。
初期表示画面自体はContentView.swift
に実装しました、
以下、ソースにコメントアウトする形で説明を入れていきます。
※ 先に1つ下の 画像(アイコン)の登録
の作業からしてもらってもOKです。
// 初期表示画面。
import SwiftUI
/**
初期表示画面。
ダナン の天気の表示とニュースのページに遷移するリンクがある
*/
struct ContentView: View {
// お天気情報 // このあと、APIでとってきたデータをセットする。
@State var result = ""
@State var temp = ""
@State var humidity = ""
@State var weathericon = ""
// 画面表示
var body: some View {
NavigationView {
VStack() {
ScrollView { // ScrollViewでスクロールを可能にする
VStack() {
Text("今日のベトナム")
.font(.largeTitle)
.fontWeight(.bold).foregroundColor(Color.white)
.lineLimit(nil)
.padding(.all, 30)
.padding(.bottom,50)
HStack{
Button(action: {}) { // actionは今は空。このあとopenweathermapにアクセスする処理を書いて呼び出す。
Image("weather").resizable().frame(width: 90, height:90)
Text("ダナンの天気")
.fontWeight(.bold).font(.title)
.foregroundColor(Color.white)
}
}
// 天気情報を取得できたときのみ、天気情報のパーツを表示させる。
if (self.result.count != 0 ){
VStack{
Group{
VStack{
Image("WeatherIcons/\(weathericon)").resizable().frame(width: 200, height: 200).padding(.all,-40)
Text("\(result)").padding(.top, 0)
}
HStack{
Text("気温 ")
.padding([.top, .leading])
Text("\(temp)")
.padding([.top, .leading])
}
HStack{
Text("湿度 ")
.padding([.top, .leading])
Text("\(humidity)")
.padding([.top, .leading])
}
}.font(.title2)
}
.padding(50)
.background(Color.white.opacity(0.7))
}
HStack{
Image("news")
.resizable()
.frame(width: 90, height: 90)
Text("ニュース")
.fontWeight(.bold).font(.title)
.foregroundColor(Color.white)
}
}
//背景画像(bg)を全画面にセットしたかったので以下のようにしてい
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
.background(Image("bg"))
}
}
}
}
/**
初期画面プレビューの表示
*/
// これがないとXcode上でプレビューを表示できないので忘れずに。
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
画像(アイコン)の登録
プロジェクト配下のAssets.xcassetsに格納します。
ネットでフリー配布されているものの色をいじって使いました。
OpenWeatherMap APIから天気情報を取得する
OpenWeatherMapへの登録
天気情報の取得はOpenWeatherMapのWeather Maps 2.0
を使いました。
会員登録すれば個人利用であれば無料で使えます。(アクセス数の上限はあり)
詳しい使い方は公式のドキュメントを読んでみてください。
シンプルな英語で書かれているので比較的読みやすいかと思います。
appidが発行できたら、天気を取得したい地域
と自身のappid
をセットしてアクセスしてきましょう。
今回はベトナムのTuranの地域(q=Turan,vn)にしました
https://api.openweathermap.org/data/2.5/weather?q=Turan,vn&appid={自身のappid}&lang=ja&units=metric
するとjsonが返却されます。
json → swift に変換する
返却されたjsonの値をquicktype に突っ込んでswift化しました。
APIの実装
こちらもソースにコメントを追加する形で説明します
// APIからお天気情報取得して格納する処理たち
/** json -> swiftに直す処理をする
https://app.quicktype.io/ で自動生成したもの
使わないものもあるので適宜削除してよし。
*/
import SwiftUI
// MARK: - Templatures
struct Templatures: Codable {
let coord: Coord
let weather: [Weather]
let base: String
let main: Main
let visibility: Int
let wind: Wind
let clouds: Clouds
let dt: Int
let sys: Sys
let timezone, id: Int
let name: String
let cod: Int
}
// MARK: - Clouds
struct Clouds: Codable {
let all: Int
}
// MARK: - Coord
struct Coord: Codable {
let lon, lat: Double
}
// MARK: - Main
struct Main: Codable {
let temp, feelsLike, tempMin: Double
let tempMax, pressure, humidity: Int
enum CodingKeys: String, CodingKey {
case temp
case feelsLike = "feels_like"
case tempMin = "temp_min"
case tempMax = "temp_max"
case pressure, humidity
}
}
// MARK: - Sys
struct Sys: Codable {
let type, id: Int
let country: String
let sunrise, sunset: Int
}
// MARK: - Weather
struct Weather: Codable {
let id: Int
let main, weatherDescription, icon: String
enum CodingKeys: String, CodingKey {
case id, main
case weatherDescription = "description"
case icon
}
}
// MARK: - Wind
struct Wind: Codable {
let speed: Double
let deg: Int
}
/**
OpenWeatherMap からお天気情報を取得する。
*/
//結果を返す関数の定義
typealias RecvFunc = ((_ item:Templatures) -> Void)?
//OpenWeatherMapお天気情報を取得するクラス
class OpenWeatherMap {
// 天気を取得する地域の指定
let locale = "Turan,vn"
// 本当はソースに直打ちではなく、設定ファイルなどで管理するべきだけど、いったん置いておく。
let appid = "自身のappid"
var _action: ((_ item:Templatures) -> Void)?
// 結果受け取り
func SetAction(action :((_ item:Templatures) -> Void)?){
self._action = action
}
// 天気情報の取得
func getWeather(action:RecvFunc) -> Void {
self.SetAction(action: action)
// APIのコール
let VNWeatherURL="https://api.openweathermap.org/data/2.5/weather?q=\(locale)&appid=\(appid)&lang=ja&units=metric"
guard let url = URL(string: VNWeatherURL)
else
{
print("URLがおかしいので処理を続けられませんでした。")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request){
data, request, error in
// 取得データをデコード
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Templatures.self, from: data){
DispatchQueue.main.async {
self._action!(decodedResponse)
}
}
}
}
// タスクの実行
.resume()
}
}
これでAPIに接続する部分はOKです。
天気のアイコンはOpenWeatherMapから取得出来ます。
私はダウンロードして./Assets.xcassets/WeatherIcons
に格納しました。
xcodeはファイル名からよしなに画像を引っ張ってきてくれます。
私はなんか嫌だったので以下の設定をしました。
これ以降のソースもこの設定をしていることが前提です
【Xcode】画像をフォルダ管理したい
次は画面のボタンを押下してこの処理を呼び出せるようにします。
// 初期表示画面。
import SwiftUI
/**
初期表示画面。
ダナン の天気の表示とニュースのページに遷移するリンクがある
*/
struct ContentView: View {
// お天気情報
@State var result = ""
@State var temp = ""
@State var humidity = ""
@State var weathericon = ""
var weatherObj = OpenWeatherMap()
//取得したお天気情報の編集
func editData(data : Templatures) {
self.result = ""
self.temp = "\(data.main.temp)℃"
self.humidity = "\(data.main.humidity)%"
// 天気データを複数取ることもあるため、1列で表示させる処理を追加)
data.weather.forEach{
item in
if (self.result.count == 0 )
{
self.result = item.weatherDescription
}else {
self.result = self.result + "、" + item.weatherDescription
}
}
// アイコンの設定
let icon = data.weather[0].icon
weathericon = icon
}
// 画面表示
var body: some View {
NavigationView {
VStack() {
ScrollView {
VStack() {
Text("今日のベトナム")
.font(.largeTitle)
.fontWeight(.bold).foregroundColor(Color.white)
.lineLimit(nil)
.padding(.all, 30)
.padding(.bottom,50)
HStack{
Button(action: {weatherObj.getWeather(action: self.editData) }) {
Image("weather").resizable().frame(width: 90, height:90)
Text("ダナンの天気")
.fontWeight(.bold).font(.title)
.foregroundColor(Color.white)
}
}
// 天気情報を取得できたときのみ、天気情報のパーツを表示させる。
if (self.result.count != 0 ){
VStack{
Group{
VStack{
Image("WeatherIcons/\(weathericon)").resizable().frame(width: 200, height: 200).padding(.all,-40)
Text("\(result)").padding(.top, 0)
}
HStack{
Text("気温 ")
.padding([.top, .leading])
Text("\(temp)")
.padding([.top, .leading])
}
HStack{
Text("湿度 ")
.padding([.top, .leading])
Text("\(humidity)")
.padding([.top, .leading])
}
}.font(.title2)
}
.padding(50)
.background(Color.white.opacity(0.7))
}
HStack{
Image("news")
.resizable()
.frame(width: 90, height: 90)
Text("ニュース")
.fontWeight(.bold).font(.title)
.foregroundColor(Color.white)
}
}.padding().frame(width: nil)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
.background(Image("bg"))
}
}
}
}
/**
初期画面プレビューの表示
*/
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
これで「ダナンの天気」ボタンを押すとAPIでデータを取得して表示が出来ます。
このAPIの呼び出しはこの部分ですね。
Buttonに呼び出すactionを設定します。以下です。
Button(action: {weatherObj.getWeather(action: self.editData) }) { ... }
最後にニュース画面の作成、画面遷移、画面遷移時のAPIの呼び出しを実装します。
ニュース画面の実装
ニュース画面への遷移はNavigationLinkを使いました
ContentView.swift
のニュースのパーツをNavigationLink
で囲みます。これは画面遷移などに使われるパーツです。
NewsView
は遷移先のニュース画面です。
HStack{
NavigationLink(destination: NewsView()) {
Image("news")
.resizable()
.frame(width: 90, height: 90)
Text("ニュース")
.fontWeight(.bold).font(.title)
.foregroundColor(Color.white)
}
}
ニュース画面の実装
画面表示時にニュースを取得するようにonAppear
を使用しました。
import SwiftUI
struct NewsView: View {
var newsMap = NewsApiMap()
@State var errorMsg = ""
@State var newsList:[(id: Int, title:String, url: String, urlToImage: String )] = []
// ニュースを取得して編集
func getData(data : News) {
newsList = []
errorMsg = ""
if(data.articles.count != 0){
newsList = []
for i:Int in (0 ... (data.articles.count - 1)){
self.newsList.append((i,data.articles[i].title, data.articles[i].url, data.articles[i].urlToImage))
}
} else {
errorMsg = "ベトナムの関連のニュースがありませんでした。\nごめんなさい。"
}
}
var body: some View{
VStack {
VStack{
Text("ニュース").font(.largeTitle).fontWeight(.bold).foregroundColor(.white)
}
.padding(.all, -5)
Spacer()
VStack{
if (self.errorMsg == ""){
List {
ForEach(newsList.indices,id: \.self) { index in
HStack{
Link("\(self.newsList[index].title)", destination: URL(string: "\(self.newsList[index].url)")!)
}
}
}
} else {
VStack{
Text("\(self.errorMsg)").font(.title).fontWeight(.bold).foregroundColor(.black)
}.frame(width: 300, height: 400, alignment: .center)
}
}.padding()
.background(Color.white.opacity(0.7))
Spacer()
}.onAppear{newsMap.getNews(action: self.getData)}
.background(Image("bg"))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
struct NewsView_Previews: PreviewProvider {
static var previews: some View {
NewsView()
}
}
## ニュースAPIから取得する処理を実装する
基本的には天気のAPIと同じです。
ニュースはNewsAPIのものを使いました。こちらも個人利用であれば無料で利用可能です。(アクセス数の制限はあります)
今回は「ベトナム」のワードを含むニュースをとってきます。
ここでは天気のAPIの実装の差分を紹介してから全文の紹介をします。
###まずは天気APIとの実装の差分
######①日本語を含むキーワードをURLに指定したい場合はaddingPercentEncodingでエンコーディングしたあげる必要がある
まとめたのでよかったらどうぞ。
【Swift】日本語を含むURLを使いたい時はaddingPercentEncodingせよ
######② quicktypeの出力結果をそのまんまは使えなかった。
よーくよーく見ればわかる部分でした。ヒントは型。
答えはこちらに書いているのでよかったらどうぞ。
【Swift】NewsAPIで取得したjsonのデコードで失敗して半日潰した話
当時はデバッグしても「デコードのあたりで落ちているや」としかわかりませんでした。
ソースの全量はこちらです。
// NewsAPIから日本語のベトナムニュースをとってくる
/** json -> swiftに直す処理をする
https://app.quicktype.io/ で自動生成したものから使うものだけ残した
*/
import SwiftUI
// MARK: - News
struct News: Codable {
let status: String
let totalResults: Int
let articles: [Article]
}
// MARK: - Article
struct Article: Codable {
let title: String
let url: String
let urlToImage: String
}
/**
NewAPI から"ベトナム"のワードを持つニュースを取得する
*/
//結果を返す関数の定義
typealias getNewsFunc = ((_ item:News) -> Void)?
//NewsApiからデータを取得するクラス
class NewsApiMap : ObservableObject {
@Published var articles: [Article] = []
let keyword = "ベトナム"
let apikey = "自身のapikey"
var _action: ((_ item:News) -> Void)?
// 結果受け取り
func SetAction(action :((_ item:News) -> Void)?){
self._action = action
}
// ニュースの取得
func getNews(action:getNewsFunc) -> Void {
self.SetAction(action: action)
// APIのコール
let NewsApiURL="https://newsapi.org/v2/everything?q=\(keyword)&num=1&sortBy=latest&apiKey=\(apikey)"
let encodedURL = NewsApiURL.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
guard let url = NSURL(string: encodedURL!) else {
print("無効なURL")
return
}
let request = URLRequest(url: url as URL)
URLSession.shared.dataTask(with: request){
data, request, error in
// 取得データをデコード
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(News.self, from: data){
DispatchQueue.main.async {
self._action!(decodedResponse)
}
}
}
}
// タスクの実行
.resume()
}
}
以上、動いて楽しかったのでここから体系的に勉強していこうと思います。