技術筆記-iOS實戰005-加入地圖和即時位置,開始好玩起來了

閱讀時間約 24 分鐘

查景點,美食,導航,這些功能已經深深融入我們的生活了,背後重要的技術支柱就是科技巨頭所提供的龐大全球地圖資料庫,和隨身手機上的 GPS 定位功能,這是 App 的強項,非玩不可。

需求情境:

在陌生的城市探索,最常用到的地圖功能是什麼?找星巴克是我的第一名,第二是享受更多功能的便利商店,再其次就是,當開車時可能要找加油站。雖然現有的地圖 App 功能已經太強大,但若能由自己手刻出來,不僅會得到掌握技術的成就感,也可以做出許多變化式,說不定真能催生商業價值。

解決方案:

搞懂 Apple 官方提供的 MapKit,實作地圖的呈現,平移放大縮小,在不同位置的搜尋,搜尋結果的標示。然後運用手機 GPS 定位功能,標出自己的位置,找到最近的目標,畫出路徑規劃,算出車程,最後用語音把重要資訊念出來。

實作步驟:

與 Map 相見歡,最好從「官方文件」下手,因為版本更新太快,問 ChatGPT 或從網路搜尋技術文章,大多已不適用了,浪費了我一些時間。首先來個 Hello World,要讓地圖從 App 顯示出來,只要一行:Map(),當然開頭需要 import MapKit:

raw-image

原來顯示地圖這麼簡單!那就趕快進行下一步,就是搜尋,假設是搜尋星巴克,怎麼做呢?

先做一個按鈕,按鈕觸發 search(),結果存在 searchResults,searchResults 必須是「狀態變數」以便直接連動 UI,所以 Map 內容必須可顯示 searchResults 的內容,使用 Marker 元件。這樣一下子增加好多東西,詳述如下:

import SwiftUI
import MapKit

extension CLLocationCoordinate2D {
    static let chunan = CLLocationCoordinate2D(latitude: 24.686793691851307, longitude: 120.88055196685008)
}

struct MapView: View {
    @State var queryString: String = ""
    @State var searchResults: [MKMapItem] = []
    @State var visibleRegion: MKCoordinateRegion?

    func search(for query: String) {
        queryString = query
        let request = MKLocalSearch.Request()
        request.naturalLanguageQuery = query
        request.resultTypes = .pointOfInterest
        request.region = visibleRegion ?? MKCoordinateRegion(
            center: .chunan,
            span: MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001)
        )
        Task {
            let search = MKLocalSearch(request: request)
            let response = try? await search.start()
            searchResults = response?.mapItems ?? []
        }
    }
    
    var body: some View {
        Map() {
            ForEach(searchResults, id: \.self) {result in
                Marker(item: result)
            }
            .annotationTitles(.hidden)   
        }
        .safeAreaInset(edge: .bottom) {
            Button {
                search(for: "星巴克")
            } label: {
                Label("星巴克", systemImage: "cup.and.saucer.fill")
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

執行搜尋必須有一個特定範圍,所以先做一個 CLLocationCoordinate2D 的 extension,加入一個靜態欄位 chunan,型別等同於它自己。這個型態是用來表示經緯度的,很重要的基本資料結構。理想情況下,地圖移到哪裡,就該搜尋哪裡;但那等一下再做,因為技術的複雜性,所以用分解動作,按部就班來。

在 search 函示中,引用了 MKLocalSearch 物件的功能,此物件可以接受一段自然語言如「星巴克」這樣的搜尋語句,加上一個描述「搜尋範圍」的參數,就是剛剛定義的 以 chunan 為中心,長寬的度量為經緯度 0.00001。搜尋必須以非同步語法啟動之,結果傳回狀態變數 searchResults,是個陣列 [MKMapItem]。在 Map() 內容中,用一段 ForEach 迴圈產生 Marker 元件,呈現效果如下:

raw-image

事情進行的很順利,然後呢?當我把地圖位置移到台北,按下按鈕,一樣搜尋竹南,這就不對了,如何讓搜尋範圍自動指定為「當前位置」?當前位置可能是在導航情境中,自動定位而得的;也可能是當使用者因特定目的而將地圖移到任何地方,此時不能強行將範圍又改回當前位置,直覺操作的背後其運作機制卻不簡單,而強大的 MapKit 又把它變簡單:

.onMapCameraChange {
context invisibleRegion =
context.region}

關鍵就是 Map 的 onMapCameraChange 事件,此事件在上面兩種情境都會觸發,加入這行就搞定了。

raw-image





移到台北,一按按鈕,真的搜尋出台北的點了,很好。

然後呢?點一下這些地點圖示,沒有反應,直覺的期望應該是,點下去顯示該地點的重點摘要說明,然後規劃路徑並畫出來,以下繼續實作。

raw-image









首先要加入選取功能,必須在 Map 的建構式中傳入 selection 參數,並綁定到狀態變數:

// MapView 開頭處加入這行​
@State var selectedResult: MKMapItem?

// ​Map 初始化時,加入 selection 參數
Map(selection: $selectedResult)

這樣一加,點下任一個地點,它就會「亮起來」,變大顆,這一切的設計都非常直覺。

接下來,我們希望它不只亮起來,也要執行「路徑規劃」,我們把此動作包裝成 getDirection(),裡面再把 getRoute() 切開成另一包,這是有回傳值得 async 函數,之後會單獨用到。路徑規劃用到 MKDirection 物件,source 先固定成 .chunan,之後抓到自己位置之後,再替換掉。


// MapView 開頭處再加入此變數
@State var route: MKRoute?

// 這兩個函數也放在 MapView 裡面​
func getRoute(dest: MKMapItem) async -> MKRoute? {
route = nil
  let request = MKDirections.Request()
  request.source = MKMapItem(placemark: MKPlacemark(coordinate: .chunan))
  request.destination = dest
  let directions = MKDirections(request: request)
  let resp = try? await directions.calculate()
  return resp?.routes.first
}

func getDirection() {
guard let selectedResult else { return }
  Task {
  route = await getRoute(dest: selectedResult)
  }
}

// Map builder 裡面畫出 route
if let route {
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
}
raw-image


畫出路徑了,讚吧!

繼續乘勝追擊,還有程式要做,就是「Where I am 」。因為移動中的使用者,必須動態的以當時位置為出發點,才有實用價值。

首先我們把地圖上系統內建的三個視覺元件叫出來:

.mapControls {
MapUserLocationButton() // 目前位置
MapCompass() // 指北針
MapScaleView() // 比例尺
}

這樣點一下地圖右上角的箭頭,就會把地圖定位在自己所在位置,但為了計算路徑,這樣還不夠,必須用程式抓出內部的經緯度資料,才有辦法客製化出即時的路徑,需要動用到 LocationManager,請看以下解析:


class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
  private var locationManager: CLLocationManager
@Published var currentLocation: CLLocation?
@Published var isUpdatingLocation: Bool = false

override init() {
locationManager = CLLocationManager()
super.init()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
}

func startUpdating() {
locationManager.startUpdatingLocation()
isUpdatingLocation = true
}
func stopUpdating() {
locationManager.stopUpdatingLocation()
isUpdatingLocation = false
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let xx = locations.last else { return }
self.currentLocation = xx
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Failed to find user's location: \(error.localizedDescription)")
}
}

此類別可控制啟動與暫停抓取位置,啟動狀態時最多一秒一筆資料,當靜止時則自動降低更新頻率。此資料對於後續變化應用很重要,因此設計讓它經常性顯示在畫面上,就放在底部:

// 在 MapView 開頭宣告狀態物件,確保在整個畫面的生命週期,都可取得物件內容 
@StateObject var locationManager = LocationManager()

// 下列視覺元件,顯示在地圖下方:
HStack {
if locationManager.isUpdatingLocation {
Button {
locationManager.stopUpdating()
} label: {
Label("更新中..", systemImage: "play.fill")
}
.buttonStyle(.borderedProminent)
} else {
Button {
locationManager.startUpdating()
} label: {
Label("停止更新", systemImage: "stop.fill")
}
.buttonStyle(.bordered)
}
if let xx = locationManager.currentLocation {
VStack {
HStack {
Text("更新時間:\(getCurrentTime())")
Spacer()
}
HStack {
Text("\(xx.coordinate.latitude) \(xx.coordinate.longitude)")
Spacer()
}
}
}


raw-image

以上程式首先為一個按鈕以啟動或停止偵測位置,旁邊簡單顯示出資料時間和抓到的經緯度。

確認經緯度資料到手後,改寫先前的 getRoute() 函式,判斷當 locationManager 有取得位置時,以它為起點來規劃路徑:

func getRoute(dest: MKMapItem) async -> MKRoute? {
route = nil
let request = MKDirections.Request()
if let xx = locationManager.currentLocation  {
request.source =  MKMapItem(placemark: MKPlacemark(coordinate: xx.coordinate))
} else {
request.source = MKMapItem(placemark: MKPlacemark(coordinate: .chunan))
}
request.destination = dest
let directions = MKDirections(request: request)
let resp = try? await directions.calculate()
return resp?.routes.first
}

呈現效果如左,至此功能架構已經完成大部分,最後稍加捏角捏邊,就要完成此專案了。



錦上添花的部分,可加入 MapKit 提供很強大的 LookAround 功能,觀看目標地點的 3D 實景,我們另做一個 View 的檔案 ItemInfoView,接受兩個參數,一樣放置在地圖下方:

if let selectedResult {
ItemInfoView(selectedResult: selectedResult, route: route)
.frame(height: 128)
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding([.top, .horizontal])
}

ItemInfoView.swift:

import SwiftUI
import MapKit

struct ItemInfoView: View {
    @State private var lookAroundScene: MKLookAroundScene?
    var selectedResult: MKMapItem
    var route: MKRoute?

    private var travelTime: String? {
        guard let route else { return nil}
        let formatter = DateComponentsFormatter()
        formatter.unitsStyle = .abbreviated
        formatter.allowedUnits = [.hour, .minute]
        return formatter.string(from: route.expectedTravelTime)
    }

    func getLookAroundScene() {
        lookAroundScene = nil
        Task {
            let request = MKLookAroundSceneRequest(mapItem: selectedResult)
            lookAroundScene = try? await request.scene
        }
    }

    var body: some View {
        LookAroundPreview(initialScene: lookAroundScene)
            .overlay(alignment: .bottomTrailing) {
                HStack {
                    Text("\(selectedResult.name ?? "")")
                    if let travelTime {
                        Text(travelTime)
                    }
                }
                .font(.caption)
                .foregroundStyle(.white)
                .padding(10)
            }
            .onAppear {
                getLookAroundScene()
            }
            .onChange(of: selectedResult) {
                getLookAroundScene()
            }
    }
}

呈現效果如下,質感整個提升起來。

raw-image

最後最後再加一個小小的變化,既然我們已經抓到本身位置,可以針對搜尋後的一系列結果,分別計算距離,找出最近的地點,用語音播放出來,這樣走在路上就方便多了。便利商店和加油站都比照辦理,三個按鈕放下去。

// 將搜尋結果的整個陣列傳入,找最近的地點,並唸出重要資訊​
// 掛在 .onChange(of: searchResults) 裡面執行
func findNearestItem(items: [MKMapItem]) {
guard let xx = locationManager.currentLocation
else {return}
if let nearestItem = items.min(
by: {$0.placemark.location!.distance(
from: locationManager.currentLocation!)
< $1.placemark.location!.distance(
from: locationManager.currentLocation!) })
{
let trimedTitle = trimLeadingNumber(s: nearestItem.placemark.title!)
let itemName = nearestItem.placemark.name!
let line1 = "最近的\(queryString) 是 \(itemName) 位於 \(trimedTitle) "
let line2 = "距離 \(String(format: "%.1f", nearestItem.placemark.location!.distance(from: xx)/1000.0)) 公里 "
Task {route = await getRoute(dest: nearestItem)
let line3 = "車程 \(String(format: "%.1f", route!.expectedTravelTime/60.0)) 分鐘"
let msg = "\(line1)\(line2)\(line3)"
print(msg)
speak(text: msg)
}
}

// 為了消除地址之前的郵遞區號,讓念出的語音減少累贅
func trimLeadingNumber(s: String) -> String {
var numberOfDigit: Int = 0
let sArray = Array(s)
for i in 0..<sArray.count {
if !sArray[i].isNumber {
numberOfDigit = ibreak
}
}
return String(s.dropFirst(numberOfDigit))
}

// 念出中文語音​,注意要執行在 main thread
func speak(text: String) {
DispatchQueue.main.async {
let utterance = AVSpeechUtterance(string: text)
utterance.voice = AVSpeechSynthesisVoice(language: "zh-TW")
utterance.rate = AVSpeechUtteranceDefaultSpeechRate
utterance.pitchMultiplier = 1.0
synthesizer.speak(utterance)
}
}

最後貼出在手機上的實際執行畫面:

raw-image

搜尋完會播放以下形式的語音:「最近的便利商店 是 全家 位於 台灣苗栗縣竹南鎮中山路166號2樓 距離 0.0 公里 車程 0.1 分鐘」。

以上,終於大功告成,真的可以拿著手機到處玩了。乾貨很多吧?誠心誠意,完整分享,敬請笑納。

Newman 2024/7/1

導覽頁:紐曼的技術筆記-索引







11會員
90內容數
漫步,悠閒自在的隨意行走!是行為,是態度,也是一種境界。
留言0
查看全部
發表第一個留言支持創作者!
newman的沙龍 的其他內容
需求情境: 為了讓多人使用 App,必須有驗證程序,以識別特定使用者,存取各自擁有的資源。 解決方案: 引用 google 所提供的雲端服務平台 Firebase,其中有多種驗證功能可選用。基於個人對 google 的偏愛,決定先採用 google signin 的方法,實作 login lo
需求情境: 一般的看盤軟體,雖然都能針對一籃子自選股票,列出其即時行情和當天漲幅,但若要看「五日漲幅」呢?那就少見了,但這對我很重要。因為小部位的波段性價差交易是個好策略,這時候若能排序好一整排看下來,可以節省大量點來點去的成本,很有價值,所以就來自己刻。 解決方案: 從大處著眼,UI 最外層
需求情境: 在設計畫面時,資料來源是後台的 api,每一次畫面細節的修修改改,都會觸發 Xcode Preview 程序,導致不斷呼叫後台。此時若資料結構和大小都具有一定規模,就會導致效率低落,不斷等待,且消耗伺服器資源甚鉅。 解決方案: 將後台傳回的資料以檔案形式暫存在本地端,每次 pr
Part.1 搞定基本的 UI 開始開發 iOS App。 首先準備一台 Mac,然後安裝 Xcode,新增專案,系統即刻生成基本的專案結構。coding 的起點在檔案 ContentView.swift: import SwiftUI struct ContentView: View {  
技術筆記-用 python 操作 google firestore 的基本方法 (2023/12/26) 技術筆記-以 nodejs 為後台,以 google sheet 充當資料庫 (2022/11/29)
firestore 是 google 所提供的雲端文件式資料庫服務,為各種開發工具提供了方便使用的 sdk,python 的套件名稱為 firebase-admin,用 pip 安裝後就可操作了。 pip install firebase-admin
需求情境: 為了讓多人使用 App,必須有驗證程序,以識別特定使用者,存取各自擁有的資源。 解決方案: 引用 google 所提供的雲端服務平台 Firebase,其中有多種驗證功能可選用。基於個人對 google 的偏愛,決定先採用 google signin 的方法,實作 login lo
需求情境: 一般的看盤軟體,雖然都能針對一籃子自選股票,列出其即時行情和當天漲幅,但若要看「五日漲幅」呢?那就少見了,但這對我很重要。因為小部位的波段性價差交易是個好策略,這時候若能排序好一整排看下來,可以節省大量點來點去的成本,很有價值,所以就來自己刻。 解決方案: 從大處著眼,UI 最外層
需求情境: 在設計畫面時,資料來源是後台的 api,每一次畫面細節的修修改改,都會觸發 Xcode Preview 程序,導致不斷呼叫後台。此時若資料結構和大小都具有一定規模,就會導致效率低落,不斷等待,且消耗伺服器資源甚鉅。 解決方案: 將後台傳回的資料以檔案形式暫存在本地端,每次 pr
Part.1 搞定基本的 UI 開始開發 iOS App。 首先準備一台 Mac,然後安裝 Xcode,新增專案,系統即刻生成基本的專案結構。coding 的起點在檔案 ContentView.swift: import SwiftUI struct ContentView: View {  
技術筆記-用 python 操作 google firestore 的基本方法 (2023/12/26) 技術筆記-以 nodejs 為後台,以 google sheet 充當資料庫 (2022/11/29)
firestore 是 google 所提供的雲端文件式資料庫服務,為各種開發工具提供了方便使用的 sdk,python 的套件名稱為 firebase-admin,用 pip 安裝後就可操作了。 pip install firebase-admin
你可能也想看
Thumbnail
「設計不僅僅是外觀和感覺。設計是其運作的方式。」 — Steve Jobs 身為一個獨立文案,許多人會以為我們的生活只需要面對電腦,從無到有,用精巧的文字填滿空白的螢幕,呈現心目中獨具風格的作品。 ——有的時候可以如此,但其實這是我們夢寐以求的偶發日常。 更多的時候,白天的工作時間總被各種繁雜
Thumbnail
台股、美股近期明顯回檔,市場敘事發生改變,壞消息一樁接一樁出現,下一步該怎麼走呢?本文將探討近期的宏觀經濟事件,並分享個人的操作思考。
Thumbnail
[曼陀號領航計畫講座心得]筆記系統太複雜難用?電腦玩物站長 Esor 分享任務導向筆記術,簡化筆記管理,輕鬆找到所需資料,發揮筆記的真正價值。
大家是否有撰寫的心血因為不明原因,網路異常而造成心血全都消失,這篇純粹是一個抒發文...
Thumbnail
提到「做筆記」你會想到什麼?如果說知識的消化可以分為「輸入、處理和輸出」三個階段,那麼你又認為筆記扮演的任務角色會落到哪一個階段?做筆記只是「輸入」成效不高,想要「輸出」又耗時費力。要如何解決這個問題呢?卡片盒筆記法,就是目前最有效的解方,特別是在數位工具智能化之後,更顯得這套方法的強大。
Thumbnail
數位筆記術的改變力量 您有曾經煩惱過學習的效率嗎?或者是覺得難以捕捉那些稍縱即逝的靈感?在我們的日常生活和學習中,這些問題都是常見的挑戰。但幸運的是,有一種工具可以幫助我們改變這些情況,那就是數位筆記術。接下來,我將會講解三個使用數位筆記術的重要原則,這些原則能幫助我們更有效地學習和捕捉靈感。 原則
Thumbnail
《花漾年華》四連幅是我用當代水墨的抽象語彙,表現臺灣春天茂盛的新綠,充滿生命力,山櫻花盛開。傳統花鳥畫的創作哲學是「一花一世界,一葉一如來」,透過描繪大自然的生機呈現藝術家空靈的精神世界,心靈與自然合一,既超越自然又貼近自然。 在手工宣紙,揮灑充滿想像力的純淨世界,留白,紓解現代人過量的影音刺激。
Thumbnail
藝術家江心靜投入創作第二年,內在巨大無比的能量爆發,完成了 2015《藍色空間》三連幅當代水墨創作。 詩人的心找到了視覺語言,用深邃迷離的藍,與宇宙的能量結合,與早年創作的詩句呼應,內外合一的文人走向天命之路。
Thumbnail
雲海流動如一首神秘的歌,興起一股衝動,想透過創作表達心中感悟,第一個想法是強調「氣韻生動」的水墨,2012年採訪現代水墨之父劉國松,高齡八十的畫家精神奕奕,親自示範技法。 看著成品,在廚房燈下微笑,黑白構成的畫面,有一股力量,從山間飛到城市,推動著自己繼續,不在意繁華如夢,一步一步,往前走。
Thumbnail
兩年多前,開始有了重新架構網站的想法,所以把 Blogger 架設的網站,移轉至到了新的平台,當年我也做過了一些優缺點評析,剛好最近又再重新整理網站的所有架構,順便也重新啟動了一次 Hugo 架設網站的循環。
Thumbnail
因為要轉換一份新工作,認為自己在工作上必需更提高效率,剛好週圍有些朋友使用電子筆記,對於喜歡寫紙本筆記的人來說,是一個新的嘗試,原因有二:其一,筆記上有很多資料,在用完整本筆記之後,隨之石沉大海;其二,現在越來越多資訊是透過網路傳送,如果沒有好好收藏,資料就"放在"硬碟內了。 我是先去Youtube
Thumbnail
「設計不僅僅是外觀和感覺。設計是其運作的方式。」 — Steve Jobs 身為一個獨立文案,許多人會以為我們的生活只需要面對電腦,從無到有,用精巧的文字填滿空白的螢幕,呈現心目中獨具風格的作品。 ——有的時候可以如此,但其實這是我們夢寐以求的偶發日常。 更多的時候,白天的工作時間總被各種繁雜
Thumbnail
台股、美股近期明顯回檔,市場敘事發生改變,壞消息一樁接一樁出現,下一步該怎麼走呢?本文將探討近期的宏觀經濟事件,並分享個人的操作思考。
Thumbnail
[曼陀號領航計畫講座心得]筆記系統太複雜難用?電腦玩物站長 Esor 分享任務導向筆記術,簡化筆記管理,輕鬆找到所需資料,發揮筆記的真正價值。
大家是否有撰寫的心血因為不明原因,網路異常而造成心血全都消失,這篇純粹是一個抒發文...
Thumbnail
提到「做筆記」你會想到什麼?如果說知識的消化可以分為「輸入、處理和輸出」三個階段,那麼你又認為筆記扮演的任務角色會落到哪一個階段?做筆記只是「輸入」成效不高,想要「輸出」又耗時費力。要如何解決這個問題呢?卡片盒筆記法,就是目前最有效的解方,特別是在數位工具智能化之後,更顯得這套方法的強大。
Thumbnail
數位筆記術的改變力量 您有曾經煩惱過學習的效率嗎?或者是覺得難以捕捉那些稍縱即逝的靈感?在我們的日常生活和學習中,這些問題都是常見的挑戰。但幸運的是,有一種工具可以幫助我們改變這些情況,那就是數位筆記術。接下來,我將會講解三個使用數位筆記術的重要原則,這些原則能幫助我們更有效地學習和捕捉靈感。 原則
Thumbnail
《花漾年華》四連幅是我用當代水墨的抽象語彙,表現臺灣春天茂盛的新綠,充滿生命力,山櫻花盛開。傳統花鳥畫的創作哲學是「一花一世界,一葉一如來」,透過描繪大自然的生機呈現藝術家空靈的精神世界,心靈與自然合一,既超越自然又貼近自然。 在手工宣紙,揮灑充滿想像力的純淨世界,留白,紓解現代人過量的影音刺激。
Thumbnail
藝術家江心靜投入創作第二年,內在巨大無比的能量爆發,完成了 2015《藍色空間》三連幅當代水墨創作。 詩人的心找到了視覺語言,用深邃迷離的藍,與宇宙的能量結合,與早年創作的詩句呼應,內外合一的文人走向天命之路。
Thumbnail
雲海流動如一首神秘的歌,興起一股衝動,想透過創作表達心中感悟,第一個想法是強調「氣韻生動」的水墨,2012年採訪現代水墨之父劉國松,高齡八十的畫家精神奕奕,親自示範技法。 看著成品,在廚房燈下微笑,黑白構成的畫面,有一股力量,從山間飛到城市,推動著自己繼續,不在意繁華如夢,一步一步,往前走。
Thumbnail
兩年多前,開始有了重新架構網站的想法,所以把 Blogger 架設的網站,移轉至到了新的平台,當年我也做過了一些優缺點評析,剛好最近又再重新整理網站的所有架構,順便也重新啟動了一次 Hugo 架設網站的循環。
Thumbnail
因為要轉換一份新工作,認為自己在工作上必需更提高效率,剛好週圍有些朋友使用電子筆記,對於喜歡寫紙本筆記的人來說,是一個新的嘗試,原因有二:其一,筆記上有很多資料,在用完整本筆記之後,隨之石沉大海;其二,現在越來越多資訊是透過網路傳送,如果沒有好好收藏,資料就"放在"硬碟內了。 我是先去Youtube