การสร้างแอปแชทแบบดั้งเดิมใน SwiftUI ในขณะที่ใช้ Swift และ WebSockets เพื่อสร้างเซิร์ฟเวอร์แชท มันรวดเร็วจากบนลงล่าง เบย์บี!
ในบทช่วยสอนนี้ เราจะสร้างแอปแชทที่ค่อนข้างเรียบง่ายแต่ใช้งานได้ แอปจะทำงานบน iOS หรือ macOS - หรือทั้งสองอย่าง! ความสวยงามของ SwiftUI คือการใช้ความพยายามเพียงเล็กน้อยในการสร้างแอปที่มีหลายแพลตฟอร์ม
แน่นอนว่าแอปแชทจะมีประโยชน์น้อยมากหากไม่มีเซิร์ฟเวอร์ให้พูดคุยด้วย ดังนั้น เราจะสร้างเซิร์ฟเวอร์แชทแบบดั้งเดิมเช่นกัน โดยใช้ WebSockets ทุกอย่างจะถูกสร้างขึ้นใน Swift และทำงานภายในเครื่องของคุณ
บทช่วยสอนนี้ถือว่าคุณมีประสบการณ์เล็กน้อยในการพัฒนาแอพ iOS/macOS โดยใช้ SwiftUI แม้ว่าเราจะอธิบายแนวคิดต่างๆ ในระหว่างดำเนินการ แต่ไม่ใช่ทุกอย่างจะครอบคลุมในเชิงลึก ไม่จำเป็นต้องพูดว่า หากคุณพิมพ์และทำตามขั้นตอน เมื่อสิ้นสุดบทช่วยสอนนี้ คุณจะมีแอปแชทที่ใช้งานได้ (สำหรับ iOS และ/หรือ macOS) ที่สื่อสารกับเซิร์ฟเวอร์ที่คุณสร้างขึ้น! คุณจะมีความเข้าใจพื้นฐานเกี่ยวกับแนวคิดต่างๆ เช่น Swift และ WebSockets ฝั่งเซิร์ฟเวอร์
หากคุณไม่สนใจเรื่องนั้น คุณสามารถเลื่อนดูจนจบและดาวน์โหลดซอร์สโค้ดสุดท้ายได้เสมอ!
กล่าวโดยสรุป เราจะเริ่มต้นด้วยการสร้างเซิร์ฟเวอร์ที่เรียบง่าย ธรรมดา และไม่มีฟีเจอร์ เราจะสร้างเซิร์ฟเวอร์เป็นแพ็คเกจ Swift จากนั้นเพิ่มเฟรมเวิร์กเว็บ Vapor เป็นการพึ่งพา ซึ่งจะช่วยให้เราตั้งค่าเซิร์ฟเวอร์ WebSocket ด้วยโค้ดเพียงไม่กี่บรรทัด
หลังจากนั้นเราจะเริ่มสร้างแอปแชทส่วนหน้า เริ่มต้นด้วยพื้นฐานอย่างรวดเร็ว จากนั้นจึงเพิ่มคุณสมบัติ (และความจำเป็น) ทีละรายการ
เวลาส่วนใหญ่ของเราจะใช้เวลาไปกับการทำงานบนแอป แต่เราจะกลับไปกลับมาระหว่างโค้ดเซิร์ฟเวอร์และโค้ดของแอปเมื่อเราเพิ่มคุณสมบัติใหม่
ไม่จำเป็น
เริ่มกันเลย!
เปิด Xcode 12 และเริ่มโครงการใหม่ ( File > New Project ) ภายใต้ Multiplatform เลือก Swift Package
เรียกแพ็คเกจว่าเป็นสิ่งที่สมเหตุสมผล - บางอย่างที่อธิบายได้ในตัว - เช่น " ChatServer " จากนั้นบันทึกทุกที่ที่คุณต้องการ
แพ็คเกจสวิฟท์?
เมื่อสร้างเฟรมเวิร์กหรือซอฟต์แวร์หลายแพลตฟอร์ม (เช่น Linux) ใน Swift แพคเกจ Swift เป็นวิธีที่นิยมใช้ เป็นโซลูชันอย่างเป็นทางการสำหรับการสร้างโค้ดโมดูลาร์ที่โปรเจ็กต์ Swift อื่นๆ สามารถใช้งานได้ง่าย Swift Package ไม่จำเป็นต้องเป็นโปรเจ็กต์แบบโมดูลาร์ แต่ก็สามารถเป็นไฟล์ปฏิบัติการแบบสแตนด์อโลนที่ใช้แพ็คเกจ Swift อื่น ๆ เป็นการพึ่งพา (ซึ่งเป็นสิ่งที่เรากำลังทำอยู่)
อาจเกิดขึ้นกับคุณว่าไม่มีโครงการ Xcode (
.xcodeproj
) สำหรับ Swift Package หากต้องการเปิด Swift Package ใน Xcode เช่นเดียวกับโปรเจ็กต์อื่นๆ เพียงเปิดไฟล์Package.swift
Xcode ควรรู้ว่าคุณกำลังเปิด Swift Package และเปิดโครงสร้างโปรเจ็กต์ทั้งหมด มันจะดึงข้อมูลการอ้างอิงทั้งหมดโดยอัตโนมัติตั้งแต่เริ่มต้นคุณสามารถอ่านเพิ่มเติมเกี่ยวกับ Swift Package และ Swift Package Manager ได้จากเว็บไซต์ Swift อย่างเป็นทางการ
เพื่อจัดการกับภาระหนักในการตั้งค่าเซิร์ฟเวอร์ เราจะใช้เฟรมเวิร์กเว็บ Vapor Vapor มาพร้อมกับคุณสมบัติที่จำเป็นทั้งหมดในการสร้างเซิร์ฟเวอร์ WebSocket
เว็บซ็อกเก็ต?
เพื่อให้เว็บสามารถสื่อสารกับเซิร์ฟเวอร์แบบเรียลไทม์ WebSockets จึงถูกสร้างขึ้น เป็นข้อกำหนดที่อธิบายไว้อย่างดีสำหรับการสื่อสารแบบเรียลไทม์ที่ปลอดภัย (แบนด์วิธต่ำ) ระหว่างไคลเอนต์และเซิร์ฟเวอร์ เช่น เกมที่มีผู้เล่นหลายคนและแอปแชท เกมที่มีผู้เล่นหลายคนในเบราว์เซอร์ที่น่าติดตามที่คุณเคยเล่นในช่วงเวลาอันมีค่าของบริษัทเหรอ? ใช่แล้ว WebSockets!
อย่างไรก็ตาม หากคุณต้องการทำบางอย่าง เช่น การสตรีมวิดีโอแบบเรียลไทม์ คุณกำลังมองหาโซลูชันอื่นที่ดีที่สุด -
แม้ว่าเราจะสร้างแอปแชท iOS/macOS ในบทช่วยสอนนี้ แต่เซิร์ฟเวอร์ที่เรากำลังสร้างสามารถสื่อสารกับแพลตฟอร์มอื่นด้วย WebSockets ได้อย่างง่ายดายพอๆ กัน แน่นอน: หากคุณต้องการ คุณสามารถสร้างแอปแชทเวอร์ชัน Android และเว็บได้ พูดคุยกับเซิร์ฟเวอร์เดียวกันและอนุญาตให้มีการสื่อสารระหว่างทุกแพลตฟอร์ม!
ไอ?
อินเทอร์เน็ตเป็นชุดท่อที่ซับซ้อน แม้แต่การตอบสนองต่อคำขอ HTTP แบบธรรมดาก็ยังต้องใช้โค้ดจำนวนมากอีกด้วย โชคดีที่ผู้เชี่ยวชาญในสาขานี้ได้พัฒนาเฟรมเวิร์กเว็บแบบโอเพ่นซอร์สที่ทำงานหนักเพื่อเรามานานหลายทศวรรษ ในภาษาการเขียนโปรแกรมต่างๆ Vapor ก็เป็นหนึ่งในนั้น และเขียนด้วยภาษา Swift มันมาพร้อมกับความสามารถของ WebSocket อยู่แล้วและเป็นสิ่งที่เราต้องการ จริงๆ
Vapor ไม่ใช่เฟรมเวิร์กเว็บที่ขับเคลื่อนด้วย Swift เพียงตัวเดียว Kitura และ Perfect ก็เป็นเฟรมเวิร์กที่รู้จักกันดีเช่นกัน แม้ว่า Vapor จะมีความกระตือรือร้นในการพัฒนามากกว่าก็ตาม
Xcode ควรเปิดไฟล์ Package.swift
ตามค่าเริ่มต้น นี่คือที่ที่เราใส่ข้อมูลทั่วไปและข้อกำหนดของแพ็คเกจ Swift ของเรา
ก่อนที่เราจะทำเช่นนั้น ให้ดูในโฟลเดอร์ Sources/ChatServer
ควรมีไฟล์ ChatServer.swift
เราจำเป็นต้องเปลี่ยนชื่อนี้เป็น main.swift
เมื่อเสร็จแล้วให้กลับไปที่ Package.swift
ภายใต้ products:
ให้ลบค่าต่อไปนี้:
. library ( name : " ChatServer " , targets : [ " ChatServer " ] )
... และแทนที่ด้วย:
. executable ( name : " ChatServer " , targets : [ " ChatServer " ] )
ท้ายที่สุดแล้ว เซิร์ฟเวอร์ของเราไม่ใช่ห้องสมุด แต่เป็นปฏิบัติการแบบสแตนด์อโลนแทน เราควรกำหนดแพลตฟอร์ม (และเวอร์ชันขั้นต่ำ) ที่เราคาดหวังให้เซิร์ฟเวอร์ของเราทำงาน ซึ่งสามารถทำได้โดยการเพิ่ม platforms: [.macOS(v10_15)]
ภายใต้ name: "ChatServer"
:
name: " ChatServer " ,
platforms: [
. macOS ( . v10_15 ) ,
] ,
ทั้งหมดนี้ควรทำให้ Swift Package ของเรา 'ทำงานได้' ใน Xcode
เอาล่ะ มาเพิ่ม Vapour เป็นการพึ่งพากันดีกว่า ใน dependencies: []
(ซึ่งควรมีบางสิ่งที่มีการใส่ความคิดเห็นไว้) ให้เพิ่มสิ่งต่อไปนี้:
. package ( url : " https://github.com/vapor/vapor.git " , from : " 4.0.0 " )
เมื่อบันทึกไฟล์ Package.swift
Xcode ควรเริ่มดึงข้อมูลการขึ้นต่อกันของ Vapor เวอร์ชัน 4.0.0
หรือใหม่กว่าโดยอัตโนมัติ เช่นเดียวกับ การ พึ่งพาทั้งหมด
เราเพียงแค่ต้องทำการปรับเปลี่ยนไฟล์อีกครั้งในขณะที่ Xcode กำลังทำสิ่งนั้น: เพิ่มการพึ่งพาให้กับเป้าหมายของเรา ใน targets:
คุณจะพบ .target(name: "ChatServer", dependencies: [])
ในอาร์เรย์ว่างนั้น ให้เพิ่มสิ่งต่อไปนี้:
. product ( name : " Vapor " , package : " vapor " )
แค่นั้นแหละ . Package.swift
ของเราเสร็จแล้ว เราได้อธิบาย Swift Package ของเราโดยบอกว่า:
Package.swift
สุดท้ายควรมีลักษณะเช่นนี้ (-ish):
// swift-tools-version:5.3
import PackageDescription
let package = Package (
name : " ChatServer " ,
platforms : [
. macOS ( . v10_15 ) ,
] ,
products : [
. executable ( name : " ChatServer " , targets : [ " ChatServer " ] )
] ,
dependencies : [
. package ( url : " https://github.com/vapor/vapor.git " , from : " 4.0.0 " )
] ,
targets : [
. target (
name : " ChatServer " ,
dependencies : [
. product ( name : " Vapor " , package : " vapor " )
] ) ,
]
)
เอาล่ะ ในที่สุดก็ถึงเวลาสำหรับ...
ใน Xcode ให้เปิด Sources/ChatServer/main.swift
และลบทุกอย่างในนั้น มันไม่มีค่าสำหรับเรา ให้ทำให้ main.swift
มีลักษณะดังนี้:
import Vapor
var env = try Environment . detect ( ) // 1
let app = Application ( env ) // 2
defer { // 3
app . shutdown ( )
}
app . webSocket ( " chat " ) { req , client in // 4
print ( " Connected: " , client )
}
try app . run ( ) // 5
- แบม! เพียงเท่านี้ก็สามารถเริ่มต้นเซิร์ฟเวอร์ (WebSocket) โดยใช้ Vapor ได้ ดูสิว่ามันง่ายดายขนาดไหน
defer
และโทร .shutdown()
ซึ่งจะทำการล้างข้อมูลเมื่อออกจากโปรแกรม/chat
ตอนนี้
เมื่อโปรแกรมทำงานได้สำเร็จ คุณอาจไม่เห็นสิ่งที่คล้ายกับแอปเลย นั่นเป็นเพราะซอฟต์แวร์เซิร์ฟเวอร์ไม่ค่อยมีส่วนต่อประสานกับผู้ใช้แบบกราฟิก แต่มั่นใจได้ว่าโปรแกรมจะมีชีวิตชีวาและอยู่เบื้องหลังโดยหมุนวงล้อ อย่างไรก็ตามคอนโซล Xcode ควรแสดงข้อความต่อไปนี้:
notice codes.vapor.application : Server starting on http://127.0.0.1:8080
ซึ่งหมายความว่าเซิร์ฟเวอร์สามารถรับฟังคำขอที่เข้ามาได้สำเร็จ เยี่ยมมาก เพราะตอนนี้เรามีเซิร์ฟเวอร์ WebSocket ที่เราสามารถเริ่มเชื่อมต่อได้!
ฉันไม่เชื่อคุณเหรอ?
หากคุณคิดว่าฉันไม่ได้พูดอะไรนอกจากคำโกหกที่ชั่วร้ายไม่ว่าจะด้วยเหตุผลใดก็ตาม คุณสามารถทดสอบเซิร์ฟเวอร์ด้วยตัวเองได้!
เปิดเบราว์เซอร์ที่คุณชื่นชอบและตรวจดูให้แน่ใจว่าคุณอยู่ในแท็บว่าง (หากเป็น Safari คุณจะต้องเปิดใช้งานโหมดนักพัฒนาซอฟต์แวร์ก่อน) เปิด ตัวตรวจสอบ (
Cmd
+Option
+I
) และไปที่ Console พิมพ์เข้ามาnew WebSocket ( 'ws://localhost:8080/chat' )และกด Return ตอนนี้ดูที่คอนโซล Xcode หากทุกอย่างเป็นไปด้วยดี ตอนนี้ควรแสดง
Connected: WebSocketKit.WebSocket
-
เซิร์ฟเวอร์สามารถเข้าถึงได้จากเครื่องของคุณเท่านั้น ซึ่งหมายความว่าคุณไม่สามารถเชื่อมต่อ iPhone/iPad จริงของคุณกับเซิร์ฟเวอร์ได้ แต่เราจะใช้โปรแกรมจำลองในขั้นตอนต่อไปนี้เพื่อทดสอบแอปแชทของเราแทน
หากต้องการทดสอบแอปแชทบนอุปกรณ์จริง จำเป็นต้องดำเนินการขั้นตอนเพิ่มเติม (เล็กน้อย) บางอย่าง อ้างถึงภาคผนวก A
แม้ว่าเราจะยังใช้งานแบ็กเอนด์ไม่เสร็จ แต่ก็ถึงเวลาที่จะย้ายไปยังฟรอนต์เอนด์ แอพแชทนั่นเอง!
ใน Xcode ให้สร้างโครงการใหม่ คราวนี้ ใต้ Multiplatform ให้เลือก App อีกครั้ง เลือกชื่อที่สวยงามสำหรับแอปของคุณแล้วดำเนินการต่อ (ฉันเลือก SwiftChat ฉันเห็นด้วย มัน สมบูรณ์แบบ ?)
แอปไม่ต้องใช้เฟรมเวิร์กหรือไลบรารีของบุคคลที่สามภายนอก แท้จริงแล้ว ทุกสิ่งที่เราต้องการมีให้ใช้งานผ่าน Foundation
, Combine
และ SwiftUI
(ใน Xcode 12+)
มาเริ่มทำงานหน้าจอแชทกันทันที สร้างไฟล์ Swift ใหม่และตั้งชื่อเป็น ChatScreen.swift
ไม่สำคัญว่าคุณจะเลือก ไฟล์ Swift หรือเทมเพลต SwiftUI View เรากำลังลบทุกอย่างในนั้นโดยไม่คำนึงถึง
นี่คือชุดเริ่มต้นของ ChatScreen.swift
:
import SwiftUI
struct ChatScreen : View {
@ State private var message = " "
var body : some View {
VStack {
// Chat history.
ScrollView { // 1
// Coming soon!
}
// Message field.
HStack {
TextField ( " Message " , text : $message ) // 2
. padding ( 10 )
. background ( Color . secondary . opacity ( 0.2 ) )
. cornerRadius ( 5 )
Button ( action : { } ) { // 3
Image ( systemName : " arrowshape.turn.up.right " )
. font ( . system ( size : 20 ) )
}
. padding ( )
. disabled ( message . isEmpty ) // 4
}
. padding ( )
}
}
}
ใน ContentsView.swift
ให้แทนที่ Hello World ด้วย ChatScreen()
:
struct ContentView : View {
var body : some View {
ChatScreen ( )
}
}
ผืนผ้าใบว่างเปล่าสำหรับตอนนี้
ซ้าย: iPhone ที่มีลักษณะสีเข้ม ขวา: iPad ที่มีลักษณะสว่าง
สิ่งที่เรามีที่นี่:
หากคุณต้องการตัดสินใจเลือกการออกแบบที่แตกต่างกัน ดำเนินการต่อได้เลย -
ตอนนี้เรามาเริ่มทำงานกับตรรกะที่ไม่เกี่ยวข้องกับ UI กันดีกว่า: การเชื่อมต่อกับเซิร์ฟเวอร์ที่เราเพิ่งสร้างขึ้น
SwiftUI ร่วมกับเฟรมเวิร์ก Combine ช่วยให้นักพัฒนามีเครื่องมือในการปรับใช้ Seperation of Concerns ในโค้ดของพวกเขาได้อย่างง่ายดาย การใช้โปรโตคอล ObservableObject
และ Wrappers คุณสมบัติ @StateObject
(หรือ @ObservedObject
) เราสามารถใช้ตรรกะที่ไม่ใช่ UI (เรียกว่า Business Logic ) ในตำแหน่งที่แยกต่างหาก อย่างที่ควรจะเป็น! ท้ายที่สุดแล้ว สิ่งเดียวที่ UI ควรใส่ใจคือการแสดงข้อมูลแก่ผู้ใช้และตอบสนองต่ออินพุตของผู้ใช้ ไม่ควรสนใจว่าข้อมูลมาจากไหนหรือถูกจัดการอย่างไร
มาจากพื้นหลังของ React ความหรูหรานี้เป็นสิ่งที่ฉันอิจฉาอย่างไม่น่าเชื่อ
มีบทความและการอภิปรายมากมายเกี่ยวกับสถาปัตยกรรมซอฟต์แวร์ คุณคงเคยได้ยินหรืออ่านเกี่ยวกับแนวคิดเช่น MVC, MVVM, VAPOR, Clean Architecture และอื่นๆ อีกมากมาย พวกเขาทั้งหมดมีข้อโต้แย้งและการประยุกต์ใช้ของพวกเขา
การพูดคุยเรื่องเหล่านี้อยู่นอกขอบเขตสำหรับบทช่วยสอนนี้ แต่โดยทั่วไปมีการตกลงกันว่าตรรกะทางธุรกิจและตรรกะ UI ไม่ควรเชื่อมโยงกัน
แนวคิดนี้เป็นจริงเช่นเดียวกับ ChatScreen ของเรา สิ่งเดียวที่ ChatScreen ควรใส่ใจคือการแสดงข้อความและจัดการข้อความที่ผู้ใช้ป้อน มันไม่สนใจ ✌️We Bs Oc K eTs✌ และไม่ควรสนใจ
คุณสามารถสร้างไฟล์ Swift ใหม่หรือเขียนโค้ดต่อไปนี้ที่ด้านล่างของ ChatScreen.swift
ทางเลือกของคุณ ไม่ว่ามันจะอยู่ที่ไหนอย่าลืม import
s!
import Combine
import Foundation
final class ChatScreenModel : ObservableObject {
private var webSocketTask : URLSessionWebSocketTask ? // 1
// MARK: - Connection
func connect ( ) { // 2
let url = URL ( string : " ws://127.0.0.1:8080/chat " ) ! // 3
webSocketTask = URLSession . shared . webSocketTask ( with : url ) // 4
webSocketTask ? . receive ( completionHandler : onReceive ) // 5
webSocketTask ? . resume ( ) // 6
}
func disconnect ( ) { // 7
webSocketTask ? . cancel ( with : . normalClosure , reason : nil ) // 8
}
private func onReceive ( incoming : Result < URLSessionWebSocketTask . Message , Error > ) {
// Nothing yet...
}
deinit { // 9
disconnect ( )
}
}
นี่อาจเป็นเรื่องที่ต้องพิจารณามาก ดังนั้นเรามาดูกันช้าๆ:
URLSessionWebSocketTask
ไว้ในพร็อพเพอร์ตี้URLSessionWebSocketTask
มีหน้าที่รับผิดชอบในการเชื่อมต่อ WebSocket พวกเขาเป็นผู้พักอาศัยในกลุ่ม URLSession
ในกรอบงาน Foundation127.0.0.1
หรือ localhost
) พอร์ตเริ่มต้นของแอปพลิเคชัน Vapor คือ 8080
และเราใส่ Listener เข้ากับการเชื่อมต่อ WebSocket ในเส้นทาง /chat
URLSessionWebSocketTask
และจัดเก็บไว้ในคุณสมบัติของอินสแตนซ์onReceive(incoming:)
จะถูกเรียก เพิ่มเติมเกี่ยวกับเรื่องนี้ในภายหลังChatScreenModel
ถูกล้างออกจากหน่วยความจำ นี่เป็นการเริ่มต้นที่ดี ตอนนี้เรามีที่ที่เราสามารถใส่ตรรกะ WebSocket ทั้งหมดของเราได้โดยไม่ทำให้โค้ด UI ยุ่งเหยิง ถึงเวลาที่ ChatScreen
จะสื่อสารกับ ChatScreenModel
เพิ่ม ChatScreenModel
เป็น State Object ใน ChatScreen
:
struct ChatScreen : View {
@ StateObject private var model = ChatScreenModel ( ) // <- this here
@ State private var message = " "
// etc...
}
เมื่อใดที่เราควรเชื่อมต่อกับเซิร์ฟเวอร์? แน่นอนว่าเมื่อมองเห็นหน้าจอ ได้จริง คุณอาจถูกล่อลวงให้โทร .connect()
ใน init()
ของ ChatScreen
นี่เป็นสิ่งที่อันตราย ในความเป็นจริงใน SwiftUI เราควรพยายามหลีกเลี่ยงการใส่สิ่งใดๆ init()
เนื่องจาก View สามารถเริ่มต้นได้แม้ว่าจะไม่ปรากฏเลยก็ตาม (เช่นใน LazyVStack
หรือใน NavigationLink(destination:)
.) คงจะน่าเสียดายถ้าต้องสิ้นเปลืองวงจร CPU อันมีค่า ดังนั้นขอเลื่อนทุกอย่างไปที่ onAppear
เพิ่มเมธอด onAppear
ให้กับ ChatScreen
จากนั้นเพิ่มและส่งเมธอดนั้นไปยังตัวแก้ไข .onAppear(perform:)
ของ VStack
:
struct ChatScreen : View {
// ...
private func onAppear ( ) {
model . connect ( )
}
var body : some View {
VStack {
// ...
}
. onAppear ( perform : onAppear )
}
}
เปลืองพื้นที่?
หลายๆ คนชอบที่จะเขียนเนื้อหาของวิธีการเหล่านี้ในบรรทัดแทน:
. onAppear { model . connect ( ) }นี่ไม่มีอะไรนอกจากความชอบส่วนตัว โดยส่วนตัวแล้วฉันชอบที่จะกำหนดวิธีการเหล่านี้แยกกัน ใช่ ต้องใช้พื้นที่มากขึ้น แต่หาได้ง่ายกว่า นำกลับมาใช้ใหม่ได้ ป้องกันไม่ให้
body
เกะกะ (มากกว่า) และพับเก็บง่ายกว่าอีกด้วย -
ในทำนองเดียวกัน เราควรตัดการเชื่อมต่อเมื่อมุมมองหายไป การใช้งานควรอธิบายได้ในตัว แต่ในกรณี:
struct ChatScreen : View {
// ...
private func onDisappear ( ) {
model . disconnect ( )
}
var body : some View {
VStack {
// ...
}
. onAppear ( perform : onAppear )
. onDisappear ( perform : onDisappear )
}
}
การปิดการเชื่อมต่อ WebSocket ทุกครั้งที่เราเลิกสนใจการเชื่อมต่อเหล่านี้เป็นสิ่งสำคัญมาก เมื่อคุณปิดการเชื่อมต่อ WebSocket (อย่างสง่างาม) เซิร์ฟเวอร์จะได้รับแจ้งและสามารถล้างการเชื่อมต่อออกจากหน่วยความจำได้ เซิร์ฟเวอร์ ไม่ ควรมีการเชื่อมต่อที่เสียหรือไม่รู้จักค้างอยู่ในหน่วยความจำ
วุ้ย. ค่อนข้างนั่งที่เราเคยผ่านมาจนถึงตอนนี้ ถึงเวลาทดสอบแล้วChatScreen
คุณจะเห็นข้อความ Connected: WebSocketKit.WebSocket
ในคอนโซล Xcode ของเซิร์ฟเวอร์ ถ้าไม่เช่นนั้น ให้ย้อนรอยขั้นตอนของคุณและเริ่มแก้ไขข้อบกพร่อง!
อีกอย่าง™️. เราควรทดสอบด้วยว่าการเชื่อมต่อ WebSocket ปิดเมื่อผู้ใช้ปิดแอป (หรือออกจาก ChatScreen
) กลับไปที่ไฟล์ main.swift
ของโครงการเซิร์ฟเวอร์ ขณะนี้ผู้ฟัง WebSocket ของเรามีลักษณะดังนี้:
app . webSocket ( " chat " ) { req , client in
print ( " Connected: " , client )
}
เพิ่มตัวจัดการให้กับ .onClose
ของ client
โดยไม่ทำอะไรเลยนอกจาก print()
:
app . webSocket ( " chat " ) { req , client in
print ( " Connected: " , client )
client . onClose . whenComplete { _ in
print ( " Disconnected: " , client )
}
}
รันเซิร์ฟเวอร์อีกครั้งและเริ่มแอปแชท เมื่อเชื่อมต่อแอปแล้ว ให้ปิดแอป (ออกจากแอปจริงๆ ไม่ใช่แค่วางไว้ในพื้นหลัง) คอนโซล Xcode ของเซิร์ฟเวอร์ควรพิมพ์ Disconnected: WebSocketKit.WebSocket
นี่เป็นการยืนยันว่าการเชื่อมต่อ WebSocket ถูกปิดจริง ๆ เมื่อเราไม่สนใจอีกต่อไป ดังนั้นเซิร์ฟเวอร์ไม่ควรมีการเชื่อมต่อที่เสียอยู่ในหน่วยความจำ
คุณพร้อมที่จะส่งบางอย่างไปยังเซิร์ฟเวอร์แล้วหรือยัง? เด็กชายฉันแน่ใจว่าเป็นเช่นนั้น แต่เพียงชั่วขณะหนึ่ง ให้เบรกและคิดสักครู่ เอนหลังบนเก้าอี้และจ้องมองอย่างไร้จุดหมาย แต่จงใจไปที่เพดาน...
เราจะส่ง อะไร ไปยังเซิร์ฟเวอร์กันแน่? และที่สำคัญไม่แพ้กันคือเราจะได้รับ อะไร กลับมาจากเซิร์ฟเวอร์บ้าง?
ความคิดแรกของคุณอาจเป็น "แค่ส่งข้อความใช่ไหม?" คุณคิดถูกเพียงครึ่งเดียว แต่แล้วเวลาของข้อความล่ะ? แล้วชื่อผู้ส่งล่ะ? แล้วตัวระบุเพื่อทำให้ข้อความไม่ซ้ำกับข้อความอื่นล่ะ? เรายังไม่มีสิ่งใดให้ผู้ใช้สร้างชื่อผู้ใช้หรืออะไรได้เลย เรามาพูดถึงเรื่องนั้นกันดีกว่าและมุ่งเน้นไปที่การส่งและรับข้อความ
เราจะต้องทำการปรับเปลี่ยนบางอย่างทั้งบนแอปและฝั่งเซิร์ฟเวอร์ เริ่มจากเซิร์ฟเวอร์กันก่อน
สร้างไฟล์ Swift ใหม่ใน Sources/ChatServer
ชื่อ Models.swift
ในโปรเจ็กต์เซิร์ฟเวอร์ วาง (หรือพิมพ์) รหัสต่อไปนี้ลงใน Models.swift
:
import Foundation
struct SubmittedChatMessage : Decodable { // 1
let message : String
}
struct ReceivingChatMessage : Encodable , Identifiable { // 2
let date = Date ( ) // 3
let id = UUID ( ) // 4
let message : String // 5
}
นี่คือสิ่งที่เกิดขึ้น:
Decodable
Encodable
ReceivingChatMessage
โปรดทราบว่าเราสร้าง date
และ id
บนฝั่งเซิร์ฟเวอร์อย่างไร สิ่งนี้ทำให้เซิร์ฟเวอร์เป็นแหล่งที่มาของความจริง เซิร์ฟเวอร์รู้ว่ากี่โมงแล้ว หากมีการสร้างวันที่ในฝั่งไคลเอ็นต์ ก็ไม่สามารถเชื่อถือได้ จะเกิดอะไรขึ้นหากลูกค้าตั้งค่านาฬิกาไว้ในอนาคต? การให้เซิร์ฟเวอร์สร้างวันที่ทำให้นาฬิกาเป็น เพียงการอ้างอิงถึงเวลาเท่านั้น
เขตเวลา?
วัตถุ
Date
ของ Swift จะมี 00:00:00 UTC 01-01-2001 เป็นเวลาอ้างอิงที่แน่นอนเสมอ เมื่อเริ่มต้นDate
หรือจัดรูปแบบเป็นสตริง (เช่นผ่านDateFormatter
) พื้นที่ของลูกค้าจะถูกนำมาพิจารณาโดยอัตโนมัติ การเพิ่มหรือการลบชั่วโมงขึ้นอยู่กับเขตเวลาของลูกค้า
UUID?
U nique Id entifiers เป็นที่ยอมรับกันทั่วโลกว่าเป็นค่าที่ยอมรับได้สำหรับตัวระบุ
นอกจากนี้เรายังไม่ต้องการให้ลูกค้าส่งข้อความหลายข้อความโดยใช้ตัวระบุที่ไม่ซ้ำกันอันเดียวกัน ไม่ว่าจะโดยบังเอิญหรือจงใจก็ตาม การให้เซิร์ฟเวอร์สร้างตัวระบุนี้ถือเป็นการรักษาความปลอดภัยเพิ่มเติมอีกชั้นหนึ่งและแหล่งที่มาของข้อผิดพลาดที่เป็นไปได้น้อยกว่า
ตอนนี้แล้ว เมื่อเซิร์ฟเวอร์ได้รับข้อความจากไคลเอนต์ ก็ควรส่งต่อไปยังไคลเอนต์อื่น ๆ ทุกตัว อย่างไรก็ตาม นี่หมายความว่าเราต้องติดตามลูกค้าทุกรายที่เชื่อมต่ออยู่
กลับไปที่ main.swift
ของโครงการเซิร์ฟเวอร์ ด้านบน app.webSocket("chat")
ใส่ข้อความประกาศต่อไปนี้:
var clientConnections = Set < WebSocket > ( )
นี่คือที่ที่เราจะจัดเก็บการเชื่อมต่อของลูกค้าของเรา
แต่เดี๋ยวก่อน ... คุณ ควร ได้รับข้อผิดพลาดในการคอมไพล์ที่ใหญ่ แย่ และน่ารังเกียจ นั่นเป็นเพราะวัตถุ WebSocket
ไม่สอดคล้องกับโปรโตคอล Hashable
ตามค่าเริ่มต้น ไม่ต้องกังวล แต่สิ่งนี้สามารถนำไปใช้ได้อย่างง่ายดาย (แม้ว่าจะถูกก็ตาม) เพิ่มโค้ดต่อไปนี้ที่ด้านล่างสุดของ main.swift
:
extension WebSocket : Hashable {
public static func == ( lhs : WebSocket , rhs : WebSocket ) -> Bool {
ObjectIdentifier ( lhs ) == ObjectIdentifier ( rhs )
}
public func hash ( into hasher : inout Hasher ) {
hasher . combine ( ObjectIdentifier ( self ) )
}
}
บาดาบิง บาดาบูม. โค้ดด้านบนเป็นวิธีที่รวดเร็วแต่ง่ายในการทำให้ class
สอดคล้องกับ Hashable
(และตามคำจำกัดความก็ Equatable
) โดยใช้ที่อยู่หน่วยความจำเป็นคุณสมบัติเฉพาะ หมายเหตุ : ใช้ได้กับชั้นเรียนเท่านั้น โครงสร้างจะต้องมีการนำไปปฏิบัติจริงเพิ่มขึ้นอีกเล็กน้อย
เอาล่ะ ตอนนี้เราสามารถติดตามลูกค้าได้แล้ว ให้แทนที่ทุกอย่างของ app.webSocket("chat")
(รวมถึงการปิด และ เนื้อหา) ด้วยโค้ดต่อไปนี้ ?:
app . webSocket ( " chat " ) { req , client in
clientConnections . insert ( client )
client . onClose . whenComplete { _ in
clientConnections . remove ( client )
}
}
เมื่อไคลเอ็นต์เชื่อมต่อ ให้จัดเก็บไคลเอ็นต์ดังกล่าวไว้ใน clientConnections
เมื่อไคลเอนต์ยกเลิกการเชื่อมต่อ ให้ลบออกจาก Set
เดียวกัน เอซซ์.
ขั้นตอนสุดท้ายในบทนี้คือการเพิ่ม หัวใจ ของเซิร์ฟเวอร์client.onClose.whenComplete
ทั้งหมด - แต่ยังคงอยู่ในการปิด app.webSocket("chat")
- เพิ่มข้อมูลโค้ดต่อไปนี้:
client . onText { _ , text in // 1
do {
guard let data = text . data ( using : . utf8 ) else {
return
}
let incomingMessage = try JSONDecoder ( ) . decode ( SubmittedChatMessage . self , from : data ) // 2
let outgoingMessage = ReceivingChatMessage ( message : incomingMessage . message ) // 3
let json = try JSONEncoder ( ) . encode ( outgoingMessage ) // 4
guard let jsonString = String ( data : json , encoding : . utf8 ) else {
return
}
for connection in clientConnections {
connection . send ( jsonString ) // 5
}
}
catch {
print ( error ) // 6
}
}
จากด้านบนอีกครั้ง:
.onText
กับไคลเอนต์ที่เชื่อมต่อReceivingChatMessage
ด้วยข้อความที่ได้รับจากไคลเอนต์ReceivingChatMessage
จะถูกสร้างขึ้นโดยอัตโนมัติReceivingChatMessage
เป็นสตริง JSON (รวมถึง Data
)เหตุใดจึงส่งคืน?
เราสามารถใช้ข้อมูลนี้เพื่อยืนยันว่าข้อความดังกล่าวได้รับจากลูกค้าเรียบร้อยแล้ว แอปจะได้รับข้อความกลับเหมือนกับที่ได้รับข้อความอื่นๆ ซึ่งจะทำให้เราไม่ต้องเขียนโค้ดเพิ่มเติมในภายหลัง
เสร็จแล้ว! เซิร์ฟเวอร์พร้อมที่จะรับข้อความและส่งต่อไปยังไคลเอนต์ที่เชื่อมต่ออื่น ๆ เรียกใช้เซิร์ฟเวอร์และปล่อยให้เซิร์ฟเวอร์ไม่ได้ใช้งานในพื้นหลัง ขณะที่เราดำเนินการแอปต่อไป!
จำโครงสร้าง SubmittedChatMessage
และ ReceivingChatMessage
ที่เราทำสำหรับเซิร์ฟเวอร์ได้ไหม เราต้องการพวกมันสำหรับแอปเช่นกัน สร้างไฟล์ Swift ใหม่และตั้งชื่อเป็น Models.swift
แม้ว่าคุณจะสามารถคัดลอกและวางการใช้งานได้ แต่จะต้องมีการแก้ไขเล็กน้อย:
import Foundation
struct SubmittedChatMessage : Encodable {
let message : String
}
struct ReceivingChatMessage : Decodable , Identifiable {
let date : Date
let id : UUID
let message : String
}
สังเกตว่ามีการสลับโปรโตคอล Encodable
และ Decodable
อย่างไร มันสมเหตุสมผลแล้ว: ในแอป เราเข้ารหัสเฉพาะ SubmittedChatMessage
และถอดรหัสเฉพาะ ReceivingChatMessage
เท่านั้น ตรงข้ามกับเซิร์ฟเวอร์ นอกจากนี้เรายังลบการกำหนดค่าเริ่มต้นอัตโนมัติของ date
และ id
ออก แอปไม่มีธุรกิจที่สร้างสิ่งเหล่านี้
โอเค กลับมาที่ ChatScreenModel
(ไม่ว่าจะเป็นไฟล์แยกหรือที่ด้านล่างของ ChatScreen.swift
ก็ตาม) เพิ่มด้านบน แต่ภายใน ChatScreenModel
เพิ่มคุณสมบัติอินสแตนซ์ต่อไปนี้:
@ Published private ( set ) var messages : [ ReceivingChatMessage ] = [ ]
ที่นี่เราจะจัดเก็บข้อความที่ได้รับ ขอบคุณ @Published
ทำให้ ChatScreen
รู้ได้อย่างชัดเจนว่าอาร์เรย์นี้ได้รับการอัปเดตเมื่อใด และจะตอบสนองต่อการเปลี่ยนแปลงนี้ private(set)
ทำให้แน่ใจว่า ChatScreenModel
เท่านั้นที่สามารถอัปเดตคุณสมบัตินี้ได้ (ท้ายที่สุดก็คือเจ้าของข้อมูล ไม่มีวัตถุอื่นใดที่มีธุรกิจใดที่จะแก้ไขมันได้โดยตรง!)
ยังอยู่ใน ChatScreenModel
ให้เพิ่มวิธีการต่อไปนี้:
func send ( text : String ) {
let message = SubmittedChatMessage ( message : text ) // 1
guard let json = try ? JSONEncoder ( ) . encode ( message ) , // 2
let jsonString = String ( data : json , encoding : . utf8 )
else {
return
}
webSocketTask ? . send ( . string ( jsonString ) ) { error in // 3
if let error = error {
print ( " Error sending message " , error ) // 4
}
}
}
ดูเหมือนอธิบายตนเองได้ แต่เพื่อความสม่ำเสมอ:
SubmittedChatMessage
ที่ตอนนี้เก็บข้อความไว้เท่านั้น เปิด ChatScreen.swift
และเพิ่มวิธีการต่อไปนี้ใน ChatScreen
:
private func onCommit ( ) {
if !message . isEmpty {
model . send ( text : message )
message = " "
}
}
วิธีการนี้จะถูกเรียกเมื่อผู้ใช้กดปุ่มส่งหรือเมื่อกด Return บนแป้นพิมพ์ แม้ว่ามันจะส่งข้อความก็ต่อเมื่อมัน มีอะไรอยู่จริงๆ เท่านั้น
ใน .body
ของ ChatScreen
ให้ค้นหา TextField
และ Button
จากนั้นแทนที่ (แต่ไม่ใช่ตัวแก้ไขหรือเนื้อหา) ด้วยการกำหนดค่าเริ่มต้นต่อไปนี้:
TextField ( " Message " , text : $message , onEditingChanged : { _ in } , onCommit : onCommit )
// .modifiers here
Button ( action : onCommit ) {
// Image etc
}
// .modifiers here
เมื่อกดปุ่ม Return ขณะที่ TextField
โฟกัสอยู่ ระบบจะเรียก onCommit
เช่นเดียวกันเมื่อผู้ใช้กด Button
TextField
ยังต้องการอาร์กิวเมนต์ onEditingChanged
แต่เราละทิ้งสิ่งนั้นด้วยการให้ปิดว่าง
ตอนนี้เป็นเวลาที่จะเริ่มทดสอบสิ่งที่เรามี ตรวจสอบให้แน่ใจว่าเซิร์ฟเวอร์ยังคงทำงานอยู่ในพื้นหลัง วางจุดพักบางส่วนในการปิด client.onText
(โดยที่เซิร์ฟเวอร์อ่านข้อความขาเข้า) ใน main.swift
ของเซิร์ฟเวอร์ เรียกใช้แอปและส่งข้อความ ควร กดเบรกพอยต์ใน main.swift
เมื่อได้รับข้อความจากแอป ถ้าเป็นเช่นนั้น ? เขียวชอุ่ม ! - ถ้าไม่เช่นนั้น... ย้อนรอยขั้นตอนของคุณและเริ่มแก้ไขจุดบกพร่อง!
ส่งข้อความก็น่ารักไปหมด แต่แล้วการรับพวกเขาล่ะ? (ในทางเทคนิคแล้ว เราได้รับสิ่งเหล่านั้น เพียงแต่ไม่เคยโต้ตอบกับสิ่งเหล่านั้นเลย) ถูกต้องแล้ว!
เรามาเยี่ยมชม ChatScreenModel
กันอีกครั้ง จำเมธอด onReceive(incoming:)
ได้ไหม? แทนที่และให้วิธีพี่น้องดังที่แสดงด้านล่าง:
private func onReceive ( incoming : Result < URLSessionWebSocketTask . Message , Error > ) {
webSocketTask ? . receive ( completionHandler : onReceive ) // 1
if case . success ( let message ) = incoming { // 2
onMessage ( message : message )
}
else if case . failure ( let error ) = incoming { // 3
print ( " Error " , error )
}
}
private func onMessage ( message : URLSessionWebSocketTask . Message ) { // 4
if case . string ( let text ) = message { // 5
guard let data = text . data ( using : . utf8 ) ,
let chatMessage = try ? JSONDecoder ( ) . decode ( ReceivingChatMessage . self , from : data )
else {
return
}
DispatchQueue . main . async { // 6
self . messages . append ( chatMessage )
}
}
}
ดังนั้น...
URLSessionWebSocketTask
? พวกเขาทำงานเพียงครั้งเดียว ดังนั้นเราจึงเชื่อมโยงตัวจัดการใหม่ทันที ดังนั้นเราจึงพร้อมที่จะอ่านข้อความขาเข้าถัดไปReceivingChatMessage
self.messages
อย่างไรก็ตาม เนื่องจาก URLSessionWebSocketTask
สามารถเรียกใช้ตัวจัดการการรับบนเธรดอื่นได้ และเนื่องจาก SwiftUI ทำงานได้เฉพาะบนเธรดหลักเท่านั้น เราจึงต้องรวมการแก้ไขของเราไว้ใน DispatchQueue.main.async {}
เพื่อให้มั่นใจว่าเรากำลังดำเนินการแก้ไขจริง ๆ บน หัวข้อหลักการอธิบายวิธีการและเหตุผลในการทำงานกับเธรดต่างๆ ใน SwiftUI นั้นอยู่นอกเหนือขอบเขตของบทช่วยสอนนี้
ใกล้จะถึงแล้ว!
กลับมาตรวจสอบอีกครั้งใน ChatScreen.swift
เห็น ScrollView
ว่างเปล่าไหม ในที่สุด เราก็สามารถเติมข้อความลงไปได้:
ScrollView {
LazyVStack ( spacing : 8 ) {
ForEach ( model . messages ) { message in
Text ( message . message )
}
}
}
มันจะดูไม่น่าตื่นเต้นแต่อย่างใด แต่นี่จะทำงานได้ในตอนนี้ เราเพียงแค่แสดงทุกข้อความด้วย o' Text
ธรรมดา
ไปข้างหน้าเรียกใช้แอป เมื่อคุณส่งข้อความ มันควรจะปรากฏบนหน้าจอทันที นี่เป็นการยืนยันว่าข้อความถูกส่งไปยังเซิร์ฟเวอร์สำเร็จ และเซิร์ฟเวอร์ก็ส่งกลับไปยังแอปได้สำเร็จ! ตอนนี้ หากทำได้ ให้เปิดแอปหลายอินสแตนซ์ (เคล็ดลับ: ใช้เครื่องจำลองที่แตกต่างกัน) แทบไม่มีการจำกัดจำนวนลูกค้า! มีปาร์ตี้แชทที่ยิ่งใหญ่ด้วยตัวเอง
ส่งข้อความต่อไปจนกว่าจะไม่มีที่ว่างบนหน้าจอ สังเกตเห็นอะไรไหม? แยร์ป. ScrollView
จะไม่เลื่อนไปที่ด้านล่างโดยอัตโนมัติเมื่อข้อความใหม่เกินขอบเขตของหน้าจอ -
เข้า...
โปรดจำไว้ว่าเซิร์ฟเวอร์จะสร้างตัวระบุที่ไม่ซ้ำกันสำหรับแต่ละข้อความ ในที่สุดเราก็สามารถนำมันไปใช้ให้เกิดประโยชน์ได้! ฉันรับรองว่าการรอคอยนั้นคุ้มค่าสำหรับการได้รับผลตอบแทนอันเหลือเชื่อนี้
ใน ChatScreen
เปลี่ยน ScrollView
ให้เป็นความงามนี้:
ScrollView {
ScrollViewReader { proxy in // 1
LazyVStack ( spacing : 8 ) {
ForEach ( model . messages ) { message in
Text ( message . message )
. id ( message . id ) // 2
}
}
. onChange ( of : model . messages . count ) { _ in // 3
scrollToLastMessage ( proxy : proxy )
}
}
}
จากนั้นเพิ่มวิธีการต่อไปนี้:
private func scrollToLastMessage ( proxy : ScrollViewProxy ) {
if let lastMessage = model . messages . last { // 4
withAnimation ( . easeOut ( duration : 0.4 ) ) {
proxy . scrollTo ( lastMessage . id , anchor : . bottom ) // 5
}
}
}
ScrollView
ไว้ใน ScrollViewReader
ScrollViewReader
ให้ proxy
ที่เราจำเป็นต้องใช้เร็วๆ นี้model.messages.count
เมื่อค่านี้เปลี่ยนแปลง เราจะเรียกเมธอดที่เราเพิ่งเพิ่มเข้าไป โดยส่งผ่าน proxy
ที่ ScrollViewReader
มอบให้.scrollTo(_:anchor:)
ของ ScrollViewProxy
สิ่งนี้จะบอก ScrollView
ให้เลื่อนไปที่มุมมองด้วยตัวระบุที่กำหนด เราล้อมสิ่งนี้ด้วย withAnimation {}
เพื่อทำให้การเลื่อนเคลื่อนไหวยังไงก็ตาม...
ข้อความเหล่านี้ค่อนข้างสมบูรณ์...แต่คงจะ ดีกว่านี้ หากเรารู้ว่าใครเป็นผู้ส่งข้อความ และแยกแยะความแตกต่างระหว่างข้อความที่ได้รับและข้อความที่ส่งด้วยสายตา
ในแต่ละข้อความ เราจะแนบชื่อผู้ใช้และตัวระบุผู้ใช้ด้วย เนื่องจากชื่อผู้ใช้ไม่เพียงพอที่จะระบุตัวผู้ใช้ เราจึงจำเป็นต้องมีบางสิ่งที่ไม่ซ้ำใคร จะเกิดอะไรขึ้นถ้าผู้ใช้และคนอื่นๆ ชื่อแพทริค? เราเกิดวิกฤตด้านข้อมูลประจำตัวและไม่สามารถแยกความแตกต่างระหว่างข้อความที่ส่งโดย Patrick และข้อความที่ได้รับจาก Patrick
ตามธรรมเนียมแล้ว เราเริ่มต้นด้วยเซิร์ฟเวอร์ ซึ่งเป็นงานน้อยที่สุด
เปิด Models.swift
โดยที่เรากำหนดทั้ง SubmittedChatMessage
และ ReceivingChatMessage
ให้ user: String
และ userID: UUID
เช่น:
struct SubmittedChatMessage : Decodable {
let message : String
let user : String // <- We
let userID : UUID // <- are
}
struct ReceivingChatMessage : Encodable , Identifiable {
let date = Date ( )
let id = UUID ( )
let message : String
let user : String // <- new
let userID : UUID // <- here
}
(อย่าลืมอัปเดตไฟล์ Models.swift ในโปรเจ็กต์ของแอปด้วย!)
กลับไปที่ main.swift
ซึ่งคุณควรจะได้รับการต้อนรับด้วยข้อผิดพลาด ให้เปลี่ยนการกำหนดค่าเริ่มต้นของ ReceivingChatMessage
เป็นดังนี้:
let outgoingMessage = ReceivingChatMessage (
message : incomingMessage . message ,
user : incomingMessage . user ,
userID : incomingMessage . userID
)
และ นั่นมัน ! เราทำเซิร์ฟเวอร์เสร็จแล้ว มันเป็นเพียงแอปต่อจากนี้ไป บ้านยืด!
ในโปรเจ็กต์ Xcode ของแอป ให้สร้างไฟล์ Swift ใหม่ชื่อ UserInfo.swift
วางรหัสต่อไปนี้ที่นั่น:
import Combine
import Foundation
class UserInfo : ObservableObject {
let userID = UUID ( )
@ Published var username = " "
}
นี่จะเป็น EnvironmentObject
ของเราที่ซึ่งเราสามารถจัดเก็บชื่อผู้ใช้ของเราได้ และเช่นเคย ตัวระบุที่ไม่ซ้ำกันจะเป็น UUID ที่ไม่เปลี่ยนรูปแบบที่สร้างขึ้นโดยอัตโนมัติ ชื่อผู้ใช้มาจากไหน? ผู้ใช้จะป้อนข้อมูลนี้เมื่อเปิดแอป ก่อนที่จะนำเสนอหน้าจอแชท
เวลาไฟล์ใหม่: SettingsScreen.swift
ไฟล์นี้จะเก็บแบบฟอร์มการตั้งค่าอย่างง่าย:
import SwiftUI
struct SettingsScreen : View {
@ EnvironmentObject private var userInfo : UserInfo // 1
private var isUsernameValid : Bool {
!userInfo . username . trimmingCharacters ( in : . whitespaces ) . isEmpty // 2
}
var body : some View {
Form {
Section ( header : Text ( " Username " ) ) {
TextField ( " E.g. John Applesheed " , text : $userInfo . username ) // 3
NavigationLink ( " Continue " , destination : ChatScreen ( ) ) // 4
. disabled ( !isUsernameValid )
}
}
. navigationTitle ( " Settings " )
}
}
UserInfo
ที่สร้างไว้ก่อนหน้านี้จะสามารถเข้าถึงได้ที่นี่ในชื่อ EnvironmentObject
TextField
จะเขียนเนื้อหาลงใน userInfo.username
โดยตรงNavigationLink
ที่จะแสดง ChatScreen
เมื่อกด ปุ่มถูกปิดใช้งานในขณะที่ชื่อผู้ใช้ไม่ถูกต้อง (คุณสังเกตไหมว่าเราเริ่มต้น ChatScreen
ใน NavigationLink
ได้อย่างไร ถ้าเราทำให้ ChatScreen
เชื่อมต่อกับเซิร์ฟเวอร์ใน init()
มันก็จะทำเช่นนั้น ทันที !)หากคุณต้องการคุณสามารถเพิ่ม การแต่งตัว สวย ๆ ให้กับหน้าจอได้
เนื่องจากเราใช้คุณสมบัติการนำทางของ SwiftUI เราจึงต้องเริ่มต้นด้วย NavigationView
ที่ไหนสักแห่ง ContentView
เป็นจุดที่สมบูรณ์แบบสำหรับสิ่งนี้ เปลี่ยนการใช้งานของ ContentView
ดังนี้:
struct ContentView : View {
@ StateObject private var userInfo = UserInfo ( ) // 1
var body : some View {
NavigationView {
SettingsScreen ( )
}
. environmentObject ( userInfo ) // 2
. navigationViewStyle ( StackNavigationViewStyle ( ) ) // 3
}
}
UserInfo
และ...EnvironmentObject
ทำให้สามารถเข้าถึงมุมมองที่ประสบความสำเร็จทั้งหมด ตอนนี้เพื่อส่งข้อมูล UserInfo
พร้อมกับข้อความที่เราส่งไปยังเซิร์ฟเวอร์ ไปที่ ChatScreenModel
(ไม่ว่าคุณจะวางไว้ที่ไหน) ที่ด้านบนของคลาสให้เพิ่มคุณสมบัติต่อไปนี้:
final class ChatScreenModel : ObservableObject {
private var username : String ?
private var userID : UUID ?
// the rest ...
}
ChatModelScreen
ควรได้รับค่าเหล่านี้เมื่อทำการเชื่อมต่อ ไม่ใช่หน้าที่ของ ChatModelScreen
ที่จะรู้ว่าข้อมูลนี้มาจากไหน หากในอนาคตเราตัดสินใจที่จะเปลี่ยนตำแหน่งที่จัดเก็บทั้ง username
และ userID
เราก็สามารถปล่อยให้ ChatModelScreen
ไม่ถูกแตะต้องได้
เปลี่ยนวิธี connect()
เพื่อยอมรับคุณสมบัติใหม่เหล่านี้เป็นอาร์กิวเมนต์:
func connect ( username : String , userID : UUID ) {
self . username = username
self . userID = userID
// etc ...
}
สุดท้ายนี้ ใน send(text:)
เราจำเป็นต้องใช้ค่าใหม่เหล่านี้กับ SubmittedChatMessage
ที่เรากำลังส่งไปยังเซิร์ฟเวอร์:
func send ( text : String ) {
guard let username = username , let userID = userID else { // Safety first!
return
}
let message = SubmittedChatMessage ( message : text , user : username , userID : userID )
// Everything else ...
}
อ๋อ แค่นั้นแหละสำหรับ ChatScreenModel
เสร็จแล้วครับ . -
เป็นครั้งสุดท้ายให้เปิด ChatScreen.swift
ที่ด้านบนของ ChatScreen
ให้เพิ่ม:
@ EnvironmentObject private var userInfo : UserInfo
อย่าลืมระบุ username
และ userID
กับ ChatScreenModel
เมื่อมุมมองปรากฏขึ้น:
private func onAppear ( ) {
model . connect ( username : userInfo . username , userID : userInfo . userID )
}
ดังเช่นที่เคยปฏิบัติมาอีกครั้ง: เอนหลังบนเก้าอี้ตัวนั้นแล้วมองขึ้นไปบนเพดาน ข้อความควรมีลักษณะ อย่างไร หากคุณไม่มีอารมณ์อยากคิดสร้างสรรค์ คุณสามารถใช้มุมมองต่อไปนี้ซึ่งแสดงถึงข้อความที่ได้รับ (และส่ง) เดียว:
struct ChatMessageRow : View {
static private let dateFormatter : DateFormatter = {
let formatter = DateFormatter ( )
formatter . dateStyle = . none
formatter . timeStyle = . short
return formatter
} ( )
let message : ReceivingChatMessage
let isUser : Bool
var body : some View {
HStack {
if isUser {
Spacer ( )
}
VStack ( alignment : . leading , spacing : 6 ) {
HStack {
Text ( message . user )
. fontWeight ( . bold )
. font ( . system ( size : 12 ) )
Text ( Self . dateFormatter . string ( from : message . date ) )
. font ( . system ( size : 10 ) )
. opacity ( 0.7 )
}
Text ( message . message )
}
. foregroundColor ( isUser ? . white : . black )
. padding ( 10 )
. background ( isUser ? Color . blue : Color ( white : 0.95 ) )
. cornerRadius ( 5 )
if !isUser {
Spacer ( )
}
}
}
}
มันไม่ได้ดูน่าตื่นเต้นเป็นพิเศษ นี่คือลักษณะที่ปรากฏบน iPhone:
(จำได้ไหมว่าเซิร์ฟเวอร์ส่งวันที่ของข้อความอย่างไร ในที่นี้ใช้เพื่อแสดงเวลา)
สีและการวางตำแหน่งจะขึ้นอยู่กับคุณสมบัติ isUser
ที่ส่งผ่านโดยผู้ปกครอง ในกรณีนี้ ผู้ปกครองนั้นไม่ใช่ใครอื่นนอกจาก ChatScreen
เนื่องจาก ChatScreen
สามารถเข้าถึงข้อความ ได้เช่นเดียว กับ UserInfo
จึงมีการวางตรรกะเพื่อพิจารณาว่าข้อความนั้นเป็นของผู้ใช้หรือไม่
ChatMessageRow
แทนที่ Text
ที่น่าเบื่อที่เราใช้ก่อนหน้านี้เพื่อแสดงข้อความ:
ScrollView {
ScrollViewReader { proxy in
LazyVStack ( spacing : 8 ) {
ForEach ( model . messages ) { message in
// This one right here ?, officer.
ChatMessageRow ( message : message , isUser : message . userID == userInfo . userID )
. id ( message . id )
}
}
// etc.
}
}
ยินดีต้อนรับสู่เส้นชัย! คุณมาถูกทางแล้ว! เป็นครั้งสุดท้ายที่
ถึงตอนนี้คุณควรมีแอปแชทแบบดั้งเดิมแต่ใช้งานได้จริง รวมถึงเซิร์ฟเวอร์ที่จัดการข้อความขาเข้าและขาออก ทั้งหมดเขียนด้วยภาษา Swift!
ยินดีด้วย! และขอบคุณมากสำหรับการอ่าน! -
คุณสามารถดาวน์โหลดโค้ดสุดท้ายได้จาก Github
ซ้าย: iPad จิ๋วที่มีรูปลักษณ์สีเข้ม ขวา: iPhone ขนาดใหญ่ที่มีรูปลักษณ์บางเบา
มาสรุปการเดินทางของเรา:
ทั้งหมดนั้นในขณะที่อยู่ในระบบนิเวศที่รวดเร็ว ไม่มีภาษาการเขียนโปรแกรมเพิ่มเติมไม่มี cocoapods หรืออะไรเลย
แน่นอนสิ่งที่เราสร้างขึ้นที่นี่เป็นเพียงเศษเสี้ยวของแอปและเซิร์ฟเวอร์แชทพร้อมการผลิตที่สมบูรณ์แบบ เราตัดมุมจำนวนมากเพื่อประหยัดเวลาและความซับซ้อน ไม่จำเป็นต้องพูดว่าควรให้ความเข้าใจพื้นฐานเกี่ยวกับวิธีการทำงานของแอพแชท
พิจารณาคุณสมบัติต่อไปนี้เพื่อนำไปใช้ด้วยตัวเอง:
ForEach
เราวนซ้ำ ทุก ข้อความในหน่วยความจำ ซอฟต์แวร์แชทสมัยใหม่ติดตามข้อความจำนวนหนึ่งเพื่อแสดงผลและโหลดในข้อความเก่า ๆ เมื่อผู้ใช้เลื่อนขึ้นurlsessionweblebetsask api แปลก ๆ
หากคุณเคยทำงานกับ WebSockets มาก่อนคุณอาจแบ่งปันความคิดเห็นว่า API ของ Apple สำหรับ WebSocket นั้นค่อนข้าง ... ไม่ใช่แบบดั้งเดิม คุณไม่ได้อยู่คนเดียวในเรื่องนี้อย่างแน่นอน การรีเบ็ดผู้ดูแลรับอย่างต่อเนื่องเป็น เรื่องแปลก หากคุณคิดว่าคุณรู้สึกสบายใจที่จะใช้ WebSocket API แบบดั้งเดิมมากขึ้นสำหรับ iOS และ MacOS ฉันจะแนะนำ StarsCream อย่างแน่นอน มันได้รับการทดสอบอย่างดีมีประสิทธิภาพและทำงานบน iOS รุ่นเก่า
ข้อบกพร่องข้อบกพร่อง
บทช่วยสอนนี้เขียนขึ้นโดยใช้ Xcode 12 Beta 5 และ iOS 14 Beta 5. ข้อบกพร่องจะปรากฏขึ้นและหายไประหว่างรุ่นเบต้าใหม่แต่ละรุ่น น่าเสียดายที่เป็นไปไม่ได้ที่จะทำนายว่าอะไรจะเกิดขึ้นและสิ่งที่จะไม่ทำงานในอนาคต (เบต้า) เผยแพร่
เซิร์ฟเวอร์ไม่เพียง แต่ทำงานบนเครื่องในเครื่องของคุณเท่านั้น แต่ยัง สามารถเข้าถึงได้ จากเครื่องในเครื่องของคุณเท่านั้น นี่ไม่ใช่ปัญหาเมื่อเรียกใช้แอพในตัวจำลอง (หรือเป็นแอพ MacOS บนเครื่องเดียวกัน) แต่การเรียกใช้แอพบนอุปกรณ์จริงหรือบน Mac อื่น ๆ เซิร์ฟเวอร์จะต้องสามารถเข้าถึงได้ในเครือข่ายท้องถิ่นของคุณ
ในการทำสิ่งนี้ใน main.swift
ของรหัสเซิร์ฟเวอร์ให้เพิ่มบรรทัดต่อไปนี้ โดยตรงหลังจาก เริ่มต้น Application
เคชัน:
app . http . server . configuration . hostname = " 0.0.0.0 "
ตอนนี้ใน ChatScreenModel
ใน connect(username:userID:)
วิธีการคุณต้องเปลี่ยน URL เพื่อให้ตรงกับ IP ในเครื่องของเครื่องของคุณ:
let url = URL ( string : " ws://127.0.0.1:8080/chat " ) !
//^^this^^^
IP ในพื้นที่ของเครื่องของคุณสามารถพบได้หลายวิธี โดยส่วนตัวแล้วฉันมักจะเปิด การตั้งค่าระบบ> เครือข่าย โดยที่ IP จะแสดงโดยตรงและสามารถเลือกและคัดลอกได้
ควรสังเกตว่าอัตราความสำเร็จของเครือข่ายนี้แตกต่างกันไป มีปัจจัยมากมาย (เช่นความปลอดภัย) ที่สามารถป้องกันไม่ให้ทำงาน
ขอบคุณมากสำหรับการอ่าน! หากคุณมีความคิดเห็นใด ๆ เกี่ยวกับงานชิ้นนี้ความคิดสำหรับการปรับปรุงหรือพบข้อผิดพลาดโปรด โปรด แจ้งให้เราทราบ! ฉันจะพยายามอย่างเต็มที่เพื่อปรับปรุงบทช่วยสอนนี้อย่างต่อเนื่อง -