Swift程式設計[第3單元] 網路程式基礎

#41 認清自己(Know yourself)

位於希臘Delphi的阿波羅神廟,曾刻有一句希臘文神諭(oracle) "γνῶθι σεαυτόν",翻譯為英文意思為 "Know yourself",「認清自己」是西方哲學非常基本的議題,也經常被後人引用,例如曾出現在「駭客任務」第一集,基努李維飾演的Neo被帶去祭師(Oracle)家裏,這句話就刻在廚房樑上。

學習物件導向也必須認清"self",因為若不懂 self,在物件導向的世界簡直就寸步難行。什麼是 "self"、為什麼需要?這對初學者來說,不只是語法問題,也是個哲學問題。

我們先用一個簡單範例,來說明物件導向程式中為什麼一定要用到 self。

假設我們想在App增加「通訊錄」功能,只包含3欄個人資料:姓名、手機號碼、好友標示,基本操作包括(「增刪查改」):

1. 新增一筆紀錄(至少需有姓名)
2. 刪除一筆紀錄(根據姓名或手機)
3. 搜尋紀錄(根據姓名或電話)
4. 更改資料(根據姓名,修改手機號碼)
5. 列印整份通訊錄

先以最容易實現的#1新增、#5列印兩功能做示範,函式化的程式如下:
// My Contacts -- functional programming
// Created by Heman, 2022/02/06

struct 個人資料 {
var 姓名: String
var 手機號碼: String
var 好友: Bool
}

func 新增(_ 人名: String, 手機: String = "", 好友嗎: Bool = false) -> 個人資料 {
return 個人資料(姓名: 人名, 手機號碼: 手機, 好友: 好友嗎)
}

func 列印通訊錄(_ 通訊錄: [個人資料]) {
for 個人 in 通訊錄 {
print(個人.姓名, 個人.手機號碼, 個人.好友 ? "❤️" : "")
}
}

var 我的通訊錄: [個人資料] = []
let 小胖 = 新增("小胖", 手機: "0912345678", 好友嗎: true)
let 小明 = 新增("小明", 手機: "0987654321")
let 小李 = 新增("小李(沒有手機)")
我的通訊錄 = 我的通訊錄 + [小胖, 小明, 小李]
列印通訊錄(我的通訊錄)


執行結果
小胖 0912345678 ❤️
小明 0987654321
小李(沒有手機)

傳統函式的概念非常簡單,就像所有的工作或流程,都可以簡化為「輸入 - 處理 - 輸出」三部分,函式也是同樣概念:


同樣的程式改為物件導向,也就是將函式包入 struct 宣告的資料類型裡面,修改如下,注意其中的函式(物件方法)如何使用 self:
// My Contacts -- object-oriented programming
// Created by Heman, 2022/02/06

struct 個人資料 {
var 姓名: String = ""
var 手機號碼: String = ""
var 好友: Bool = false
}

struct 通訊錄 {
var 內容: [個人資料] = []
mutating func 新增(_ 人名: String, 手機: String = "", 好友嗎: Bool = false) {
self.內容 = self.內容 + [個人資料(姓名: 人名, 手機號碼: 手機, 好友: 好友嗎)]
}
func 列印() {
for 個人 in self.內容 {
print(個人.姓名, 個人.手機號碼, 個人.好友 ? "❤️" : "")
}
}
}

var 我的通訊錄 = 通訊錄()
我的通訊錄.新增("小胖", 手機: "0912345678", 好友嗎: true)
我的通訊錄.新增("小明", 手機: "0987654321")
我的通訊錄.新增("小李(沒有手機)")
我的通訊錄.列印()


執行結果完全相同
小胖 0912345678 ❤️
小明 0987654321
小李(沒有手機)

物件方法的概念稍微比較複雜一些,除了輸出、輸入之外,還多了內部可以存取的屬性,在範例中,「新增()」目的是修改物件自己的屬性,所以不需要回傳值(輸出)。


簡單地說,內部存取的屬性,不管是要讀出還是要寫入,在物件方法裡就用 "self.屬性名稱" 來取用,而且在名稱不會混淆的情況下,這裡的 self 都可以省略,直接寫「屬性名稱」即可,也就是:
 self.內容 = self.內容 + [個人資料(姓名: 人名, 手機號碼: 手機, 好友: 好友嗎)]

也可以寫成:
 內容 = 內容 + [個人資料(姓名: 人名, 手機號碼: 手機, 好友: 好友嗎)]


總結起來,函式化方法是將資料(輸出、輸入)與工作(函式)分開,彼此獨立,觀念簡單明瞭,解決簡單的問題快又有效;物件導向的方法將某些資料與動作包在一起,外部的輸出、輸入仍然可以有,但重點放在內部資料(也就是物件屬性)的操作上,可以建構出強大物件來解決複雜問題。

註解
  1. self是自己、本身之意。在德國哲學體系中,將self細分為 ego「自我」,是自我意識的本體;人還有潛意識下的「本我」,稱為 id ,控制著隱藏在意識之下的慾望、情緒與本能;理性的部分稱為 super-ego「超我」,經過後天有意識的學習,昇華為道德、社會倫理、理智之所在。
CUNNING
之前看到self都不知道在幹嘛,原來是這樣用的
#42 物件類型(Type)與實例(instance)

self 還有一個容易疑惑之處,就是self 雖然寫在宣告 struct 物件類型的函式裡,但卻是在物件實例中才成形並生效。要了解這種情形,就必須進一步了解類型(Type)與實例(instance)的特性。

在第1單元一開始介紹過「資料類型」與「變數常數」的關係,例如 var i: Int = 10 宣告變數 i 屬於 Int整數類型,初始值指定為10,常數則用 let 宣告。

變數常數可比喻成一個能放東西的「盒子」,資料類型決定盒子外型,整數類型的盒子只能放整數值,字串類型的盒子只能放字串值;而變數與常數的區別,是變數盒子可以隨時抽換新內容,常數只能放一次,不能再更換。

對應到電腦硬體,「盒子」其實就是一個記憶體空間,例如一個64位元的整數變數,會佔據固定8位元組(byte)的記憶體空間,而一個字串變數,則隨其字串值長短動態配置記憶體空間。

當我們宣告一個常數變數時,作業系統通常就會給出一個該類型的盒子,即配置所需的記憶體空間。

物件也有類似的關係,物件實例(instance)就是屬於某物件類型(Type)的變數或常數。只不過物件可以用 struct 來自行定義新類型,例如上一節我們定義了「個人資料」與「通訊錄」兩種物件類型:
struct 個人資料 {    //物件類型
var 姓名: String = ""
var 手機號碼: String = ""
var 好友: Bool = false
}

struct 通訊錄 { //物件類型
var 內容: [個人資料] = []
mutating func 新增(_ 人名: String, 手機: String = "", 好友嗎: Bool = false) {
self.內容 = self.內容 + [個人資料(姓名: 人名, 手機號碼: 手機, 好友: 好友嗎)]
}
func 列印() {
for 個人 in self.內容 {
print(個人.姓名, 個人.手機號碼, 個人.好友 ? "❤️" : "")
}
}
}

在 struct 宣告類型(Type)時,雖然裡面用 var/let 定義了變數常數,但並不會馬上配置記憶體。類型宣告其實只是定義盒子外型 -- 「個人資料」內含3個字串變數,相當於1個大盒子內嵌3個小盒子,兩個放字串,一個放布爾(Bool)值。

當宣告此類型的變數常數時,才會產出符合該類型的盒子,例如:
var 我的通訊錄 = 通訊錄() //物件實例

這時候「我的通訊錄」才會獲得一個盒子(記憶體空間),有著「通訊錄」類型的外觀。「通訊錄」類型裡有陣列變數「內容」、有函式「新增」「列印」,這類型的「盒子」會長什麼樣呢?

陣列就是大盒子套許多一樣的小盒子,陣列有幾個元素就套幾個盒子;函式比較特別,函式程式碼就像機器,會集中在另外一個地方(暫喻為「工廠」),不放在資料盒子裡,但「通訊錄」這類大盒子裡會有兩個特殊卡片 -- 可以去工廠使用「新增」與「列印」兩部機器的憑據。

所以物件實例「我的通訊錄」會是一個大盒子,裡面有n個盒子放「內容」陣列(空陣列時n=0),還有兩個卡片,允許使用工廠裡的「新增」與「列印」。

當執行過:
我的通訊錄.新增("小胖", 手機: "0912345678", 好友嗎: true)

「我的通訊錄」大盒子裡面會增加一個小盒子,放小胖的「個人資料」值。當「我的通訊錄」拿著卡片去工廠使用「新增」時,「新增」函式的程式碼會取用 self -- 這裡 self 代表的是物件實例「我的通訊錄」,而不是物件類型「通訊錄」,示意圖如下:


物件在初始化時,只須針對「屬性」(變數常數)給予初始值,方法(函式)不需要初始化,以下範例我們要幫「個人資料」寫一個extension,增加「更換」方法,注意其中 self 的語法:
// My Contacts -- "self" usage
// Revised by Heman, 2022/02/10

struct 個人資料 {
var 姓名: String = ""
var 手機號碼: String = ""
var 好友: Bool = false
}

extension 個人資料 {
mutating func 更換(名: String, 號: String, 友: Bool) {
self = 個人資料(姓名: 名, 手機號碼: 號, 好友: 友)
}
}

struct 通訊錄 {
var 內容: [個人資料] = []
mutating func 新增(_ 人名: String, 手機: String = "", 好友嗎: Bool = false) {
self.內容 = self.內容 + [個人資料(姓名: 人名, 手機號碼: 手機, 好友: 好友嗎)]
}
func 列印() {
for 個人 in self.內容 {
print(個人.姓名, 個人.手機號碼, 個人.好友 ? "❤️" : "")
}
}
}

var 我的通訊錄 = 通訊錄()
我的通訊錄.新增("小胖", 手機: "0912345678", 好友嗎: true)
我的通訊錄.新增("小明", 手機: "0987654321")
我的通訊錄.新增("小李(沒有手機)")
我的通訊錄.內容[0].更換(名: "小白", 號: "0911123456", 友: false)
我的通訊錄.列印()

執行結果在主控台輸出:
小白 0911123456 
小明 0987654321
小李(沒有手機)

第1筆紀錄已從小胖更換為小白。當使用「更換」方法時,通訊錄內容的該筆紀錄(範例為第1筆「內容[0]」)會更換為新的「個人資料」值,這時候的 self 指的是「內容[0]」這個「個人資料」類型的物件實例:
self = 個人資料(姓名: 名, 手機號碼: 號, 好友: 友)

範例3-10a用JSON解碼器更新物件實例也是採取同樣方式。

註解
  1. 為什麼西方哲學特別重視「自我(self)」?因為人類之所以異於其他生物,產生智慧文明,很可能與個體(instance)的自我意識(self-awareness)有關,螞蟻蜜蜂雖然高度社會化,也能分工合作,但缺乏個體自我意識,僅能遵照本能行事,無法發展文明;哺乳動物大多有自我意識,但以人類自我意識最強(所謂「自私基因」)、智慧最高,因此每個個體都能獨立發展,既競爭又能合作,進而催生人類文明。
  2. 物件類型就像人類,類型宣告的變數常數(物件屬性)與函式(物件方法),就像基因或設計圖,人類基因包含自我(self),顯然這個自我指的是個人(instance),而不是整個人類(Type)。
  3. 當物件類型初始化產出物件實例時,就像利用基因繁殖出一個個體,與我們之前將物件類型比喻為「模子」是同樣道理。
#43 第3單元結語

經過5個月的努力,終於將第3單元「Swift網路程式設計」寫完了,雖然中間花了一個多月等待Swift Playgrounds 4.0,所幸新的 async/await 語法超乎想像的好用,在iPad版的Swift Playgrounds 4.0 上面寫App也夢想成真,證明這個等待是值得的。

async/await 並不是唯一能寫非同步程式的指令,實現並行作業或非同步程式的方法一直都有,之前Swift用的方法稱為 Grand Central Dispatch (GCD),但顯然 async/await 更易用、效能更好。

其實網路世界不斷會有新技術出現,大多情況毋須特意等待,因為新技術就像潮水,一波接一波永遠等不完,真的需要時,就拿現有技術去解決問題,才是正道。而當新技術出現時,則應盡快採用,才能維持競爭優勢,否則很容易被後浪超越。

程式語言是屬於技術底層的基礎設施,相對其他科技來說變化不會太快,像 C/C++, Java, Python, Javascript 等語言,都已沿用20-30年,如果是手機或電腦,誰還在用20年前的產品?這對學習程式語言來說是件好事,在日新月異的數位時代,學習一門可以用20年的技術,顯然是很聰明的投資。

至於Swift 網路程式設計,本單元只算是入門基礎,學會如何抓網路圖片、JSON資料或播放音樂,以單向下載為主。進階的網路程式,必須還能上傳檔案或資料,甚至像手機遊戲一樣快速互動,這牽涉到雲端伺服器的程式設計,雖然Swift也能夠寫伺服器端(後端)的程式,例如用vapor 套件,但更合適的可能是 Go, Python 或 Node.js (Javascript) 之類的程式語言,再搭配資料庫的設計等等,總之,網路程式進階的空間還很大。

按照最初的規劃,接下來第4單元會回到SwiftUI的平面繪圖,介紹 GeometryReader, Path, Shape等核心物件,以及兩個去年SwiftUI新增的功能,TimelineView 與 Canvas,這兩個物件看起來非常強大,很吸引人。

不過筆者沒有把握什麼時候會開始寫,最慢應該在今年暑假之前吧。

後記:第4單元 SwiftUI 動畫與繪圖
CUNNING
三個單元總共有511頁A4,可以出書了
雪白西丘斯
第4單元「SwiftUI 動畫與繪圖」已完成,網址連結如上
請教一個基本問題,如何查詢某個網站JSON資料的連結,如yahoo新聞tw.news.yahoo.com
雪白西丘斯
Yahoo新聞轉載很多其他媒體的文章,受限於著作權的關係,可能不會開放API抓新聞內容,個人猜測,僅供參考。
jasonhsu0076
謝謝老師,我再試試查找其他網站
雪白西丘斯 wrote:
#37 第10課 逐(恕刪)



請教一下我要做個很多單位的通訊錄,大家的機關名稱跟地址都是相同的
要做出巢狀的json,利用google的export sheet data要怎麼做出來?

https://docs.google.com/spreadsheets/d/1FqlrZ-rNspecUHAPwuKNlxwoMd_mfwj84zJ6kCUJ5sU/edit?usp=sharing
類似這樣嗎?

[
{
"機關名稱": "A機關",
"機關地址": "A地址",
"通訊錄":
[
{
"職稱": "",
"姓名": "A",
"電話": "A"
},
{
"職稱": "",
"姓名": "B",
"電話": "B"
},
{
"職稱": "",
"姓名": "C",
"電話": "C"
}
]
},
{
"機關名稱": "B機關",
"機關地址": "B地址",
"通訊錄":
[
{
"職稱": "",
"姓名": "L",
"電話": "L"
},
{
"職稱": "",
"姓名": "M",
"電話": "M"
},
{
"職稱": "",
"姓名": "N",
"電話": "N"
}
]
}
]
CUNNING
對,就是這樣子
CUNNING
老師這個兩層的Json我可以用vba來搞定了
CUNNING wrote:
老師再請教一下,ipad...(恕刪)


這個關鍵字加粗變黑的效,是不是老師的4-2a 帶屬性文字的這個用法?

var 帶屬性字串 = AttributedString(訊息)
if let 文字範圍 = 帶屬性字串.range(of: "🀁") {
帶屬性字串[文字範圍].backgroundColor = .red
帶屬性字串[文字範圍].foregroundColor = .white
}



這個關鍵字加粗變黑的效,是不是老師的4-2a 帶屬性文字的這個用法?

var 帶屬性字串 = AttributedString(訊息)
if let 文字範圍 = 帶屬性字串.range(of: "🀁") {
帶屬性字串[文字範圍].backgroundColor = .red
帶屬性字串[文字範圍].foregroundColor = .white
}
雪白西丘斯
沒錯,應該可以做得出來。
通訊錄搜尋並顯示粗體

試做結果如下,名字或email都可以搜尋:


AttributedString 用法請參考第4單元4-2a 帶屬性文字

// 搜尋結果並顯示粗體(AttributedString)
// Tested by Heman, 2022/06/03
import PlaygroundSupport
import SwiftUI

struct 個資: Identifiable {
var id = UUID()
var name: AttributedString = ""
var email: AttributedString = ""
}

let 通訊錄: [個資] = [
個資(name: "老王", email: "[email protected]"),
個資(name: "老李", email: "[email protected]"),
個資(name: "小張", email: "[email protected]"),
個資(name: "小明", email: "[email protected]")
]

struct 我的通訊錄: View {
@State var keyword: String = ""
var body: some View {
NavigationView {
List(搜尋結果) { 客戶 in
VStack(alignment: .leading, spacing: 0) {
Text(客戶.name)
.font(.title3)
Text(客戶.email)
}
}
.searchable(text: $keyword, placement: .navigationBarDrawer(displayMode: .always))
.navigationTitle("通訊錄")
}
}
var 搜尋結果: [個資] {
if keyword == "" {
return 通訊錄
} else {
var 名單: [個資] = []
for i in 通訊錄.indices {
if let 範圍 = 通訊錄[i].name.range(of: keyword) {
var 個人 = 個資()
個人.name = 通訊錄[i].name
個人.name[範圍].font = .system(.title3).bold()
個人.name[範圍].foregroundColor = .blue
個人.email = 通訊錄[i].email
if let 範圍 = 個人.email.range(of: keyword) {
個人.email[範圍].font = .system(.body).bold()
個人.email[範圍].foregroundColor = .blue
}
名單 += [個人]
} else if let 範圍 = 通訊錄[i].email.range(of: keyword) {
var 個人 = 個資()
個人.name = 通訊錄[i].name
個人.email = 通訊錄[i].email
個人.email[範圍].font = .system(.body).bold()
個人.email[範圍].foregroundColor = .blue
名單 += [個人]
}
}
return 名單
}
}
}

PlaygroundPage.current.setLiveView(我的通訊錄())
雪白西丘斯 wrote:
通訊錄搜尋並顯示粗體(恕刪)


 

import SwiftUI

struct 個資: Identifiable {
var id = UUID()
var name = ""
var email = ""
}

var 通訊錄: [個資] = [
個資(name: "老王", email: "[email protected]"),
個資(name: "老李", email: "[email protected]"),
個資(name: "小張", email: "[email protected]"),
個資(name: "小明", email: "[email protected]")
]


struct 我的通訊錄: View {
@State var keyword: String = ""
@State var 新通訊錄: [個資] = 通訊錄
var body: some View {
NavigationView {
List(新通訊錄) { 客戶 in
let 客戶的name: AttributedString = {
do {
var text = try AttributedString(客戶.name)
if let range = text.range(of: keyword) {
text[range].backgroundColor = .yellow
}
return text

} catch {
return ""
}
}()
let 客戶的email: AttributedString = {
do {
var text = try AttributedString(客戶.email.localizedLowercase)
if let range = text.range(of: keyword.localizedLowercase) {
text[range].backgroundColor = .yellow
}
return text

} catch {
return ""
}
}()
VStack(alignment: .leading, spacing: 0) {
Text(客戶的name)
.font(.title3)
Text(客戶的email)
}
}
.searchable(text: $keyword, placement: .navigationBarDrawer(displayMode: .always))
.navigationTitle("通訊錄")
.onChange(of: keyword) { keyword in
if keyword == "" {
新通訊錄 = 通訊錄
} else {
新通訊錄 = 通訊錄.filter {
return $0.name.contains(keyword) || $0.name.localizedCaseInsensitiveContains(keyword) || $0.email.contains(keyword) || $0.email.localizedCaseInsensitiveContains(keyword)
}
}
}
}
}

/*
var 搜尋結果: [個資] {
if keyword == "" {
return 通訊錄
} else {
var 名單: [個資] = []
for i in 通訊錄.indices {
if let 範圍 = 通訊錄[i].name.range(of: keyword) {
var 個人 = 個資()
個人.name = 通訊錄[i].name
個人.name[範圍].font = .system(.title3).bold()
個人.name[範圍].foregroundColor = .blue
個人.email = 通訊錄[i].email
if let 範圍 = 個人.email.range(of: keyword) {
個人.email[範圍].font = .system(.body).bold()
個人.email[範圍].foregroundColor = .blue
}
名單 += [個人]
} else if let 範圍 = 通訊錄[i].email.range(of: keyword) {
var 個人 = 個資()
個人.name = 通訊錄[i].name
個人.email = 通訊錄[i].email
個人.email[範圍].font = .system(.body).bold()
個人.email[範圍].foregroundColor = .blue
名單 += [個人]
}
}
return 名單
}

}
*/
}




我改這個也可以變色了
雪白西丘斯
不錯不錯,還能不分大小寫一起搜尋,更方便。
CUNNING
onChange這段也是老師寫的啦,在這單元有問過
關閉廣告
文章分享
評分
評分
複製連結

今日熱門文章 網友點擊推薦!