การพัฒนาแอพพลิเคชั่นบน SwiftUI ส่วนที่ 1: Dataflow และ Redux

การพัฒนาแอพพลิเคชั่นบน SwiftUI ส่วนที่ 1: Dataflow และ Redux

หลังจากเข้าร่วมเซสชั่น State of the Union ที่ WWDC 2019 ฉันตัดสินใจเจาะลึกเกี่ยวกับ SwiftUI ฉันใช้เวลาทำงานกับมันมามาก และตอนนี้ได้เริ่มพัฒนาแอปพลิเคชันจริงที่เป็นประโยชน์กับผู้ใช้ในวงกว้างแล้ว

ฉันเรียกมันว่า MovieSwiftUI - นี่คือแอปสำหรับค้นหาภาพยนตร์ใหม่และเก่ารวมถึงรวบรวมไว้ในคอลเลกชันโดยใช้ TMDB API. ฉันรักภาพยนตร์มาโดยตลอดและยังสร้างบริษัทที่ทำงานด้านนี้ด้วยแม้จะนานมาแล้วก็ตาม บริษัทแทบจะเรียกได้ว่าเจ๋งไม่ได้เลย แต่แอปพลิเคชั่นนั้นเจ๋งมาก!

เราเตือนคุณ: สำหรับผู้อ่าน "Habr" ทุกคน - ส่วนลด 10 rubles เมื่อลงทะเบียนในหลักสูตร Skillbox ใด ๆ โดยใช้รหัสส่งเสริมการขาย "Habr"

Skillbox แนะนำ: หลักสูตรออนไลน์เพื่อการศึกษา "นักพัฒนาจาวามืออาชีพ".

MovieSwiftUI สามารถทำอะไรได้บ้าง?

  • โต้ตอบกับ API - แอปพลิเคชันสมัยใหม่เกือบทุกตัวทำเช่นนี้
  • โหลดข้อมูลแบบอะซิงโครนัสตามคำขอและแยกวิเคราะห์ JSON ลงในโมเดล Swift โดยใช้ เข้ารหัสได้.
  • แสดงภาพที่โหลดตามคำขอและแคชไว้
  • แอพสำหรับ iOS, iPadOS และ macOS นี้มอบ UX ที่ดีที่สุดสำหรับผู้ใช้ OS เหล่านี้
  • ผู้ใช้สามารถสร้างข้อมูลและสร้างรายการภาพยนตร์ของตนเองได้ แอปพลิเคชันบันทึกและกู้คืนข้อมูลผู้ใช้
  • มุมมอง ส่วนประกอบ และแบบจำลองจะถูกแยกออกจากกันอย่างชัดเจนโดยใช้รูปแบบ Redux การไหลของข้อมูลที่นี่เป็นแบบทิศทางเดียว สามารถแคช กู้คืน และเขียนทับได้อย่างสมบูรณ์
  • แอปพลิเคชันใช้ส่วนประกอบพื้นฐานของ SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal ฯลฯ นอกจากนี้ยังมีมุมมอง ท่าทาง UI/UX แบบกำหนดเองอีกด้วย

การพัฒนาแอพพลิเคชั่นบน SwiftUI ส่วนที่ 1: Dataflow และ Redux
ที่จริงแล้วแอนิเมชั่นนั้นราบรื่น GIF กลับกลายเป็นกระตุกเล็กน้อย

การทำงานกับแอปทำให้ฉันได้รับประสบการณ์มากมาย และโดยรวมแล้วถือเป็นประสบการณ์ที่ดี ฉันสามารถเขียนแอปพลิเคชันที่มีฟังก์ชันการทำงานเต็มรูปแบบได้ ในเดือนกันยายนฉันจะปรับปรุงและเผยแพร่ใน AppStore พร้อมกับการเปิดตัว iOS 13

Redux, BindableObject และ EnvironmentObject

การพัฒนาแอพพลิเคชั่นบน SwiftUI ส่วนที่ 1: Dataflow และ Redux

ฉันทำงานกับ Redux มาประมาณสองปีแล้ว ดังนั้นฉันจึงค่อนข้างเชี่ยวชาญเรื่องนี้ โดยเฉพาะผมใช้มันในส่วนหน้าสำหรับ เกิดปฏิกิริยา เว็บไซต์ตลอดจนการพัฒนาแอปพลิเคชัน iOS (Swift) และ Android (Kotlin) แบบเนทีฟ

ฉันไม่เคยเสียใจที่เลือก Redux เป็นสถาปัตยกรรมการไหลของข้อมูลสำหรับการสร้างแอปพลิเคชัน SwiftUI ส่วนที่ท้าทายที่สุดเมื่อใช้ Redux ในแอป UIKit คือการทำงานร่วมกับร้านค้าและรับและดึงข้อมูลและแมปกับมุมมอง/ส่วนประกอบของคุณ ในการทำเช่นนี้ ฉันต้องสร้างไลบรารีตัวเชื่อมต่อชนิดหนึ่ง (โดยใช้ ReSwift และ ReKotlin) ใช้งานได้ดีแต่โค้ดค่อนข้างเยอะ น่าเสียดายที่มันยังไม่ใช่โอเพ่นซอร์ส (ยัง)

ข่าวดี! สิ่งเดียวที่ต้องกังวลกับ SwiftUI - หากคุณวางแผนที่จะใช้ Redux - คือร้านค้า รัฐ และตัวลดขนาด การโต้ตอบกับร้านค้าได้รับการดูแลอย่างสมบูรณ์โดย SwiftUI ต้องขอบคุณ @EnvironmentObject ดังนั้น store เริ่มต้นด้วย BindableObject

ฉันสร้างแพ็คเกจ Swift ง่ายๆ SwiftUIFluxซึ่งให้การใช้งานพื้นฐานของ Redux ในกรณีของฉัน มันเป็นส่วนหนึ่งของ MovieSwiftUI ฉันด้วย ได้เขียนบทช่วยสอนทีละขั้นตอนซึ่งจะช่วยให้คุณใช้ส่วนประกอบนี้ได้

มันทำงานอย่างไร

final public class Store<State: FluxState>: BindableObject {
    public let willChange = PassthroughSubject<Void, Never>()
        
    private(set) public var state: State
    
    private func _dispatch(action: Action) {
        willChange.send()
        state = reducer(state, action)
    }
}

ทุกครั้งที่คุณกระตุ้นการทำงาน คุณจะเปิดใช้งานกระปุกเกียร์ โดยจะประเมินการดำเนินการตามสถานะปัจจุบันของแอปพลิเคชัน จากนั้นจะส่งคืนสถานะที่แก้ไขใหม่ตามประเภทการดำเนินการและข้อมูล

เนื่องจาก store เป็น BindableObject จึงจะแจ้งเตือน SwiftUI เมื่อค่าของมันเปลี่ยนแปลงโดยใช้คุณสมบัติ willChange ที่จัดทำโดย PassthroughSubject เนื่องจาก BindableObject ต้องจัดเตรียม PublisherType แต่การนำโปรโตคอลไปใช้มีหน้าที่ในการจัดการ โดยรวมแล้วนี่เป็นเครื่องมือที่ทรงพลังมากจาก Apple ดังนั้นในรอบการเรนเดอร์ถัดไป SwiftUI จะช่วยเรนเดอร์เนื้อหาของมุมมองตามการเปลี่ยนแปลงสถานะ

จริงๆ แล้วนี่คือหัวใจและความมหัศจรรย์ของ SwiftUI ในตอนนี้ ในมุมมองใดก็ตามที่สมัครรับข้อมูลสถานะ มุมมองจะถูกเรนเดอร์ตามข้อมูลที่ได้รับจากรัฐและสิ่งที่เปลี่ยนแปลงไป

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
 
    var window: UIWindow?
 
 
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            
            let controller = UIHostingController(rootView: HomeView().environmentObject(store))
            window.rootViewController = controller
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}
 
struct CustomListCoverRow : View {
    @EnvironmentObject var store: Store<AppState>
    
    let movieId: Int
    var movie: Movie! {
        return store.state.moviesState.movies[movieId]
    }
    
    var body: some View {
        HStack(alignment: .center, spacing: 0) {
            Image(movie.poster)
        }.listRowInsets(EdgeInsets())
    }
}

Store จะถูกแทรกเป็น EnvironmentObject เมื่อแอปพลิเคชันเริ่มทำงาน และสามารถเข้าถึงได้จากมุมมองใดก็ได้โดยใช้ @EnvironmentObject ไม่มีการลงโทษด้านประสิทธิภาพเนื่องจากคุณสมบัติที่ได้รับจะถูกดึงหรือคำนวณอย่างรวดเร็วจากสถานะแอปพลิเคชัน

โค้ดด้านบนจะเปลี่ยนรูปภาพหากโปสเตอร์ภาพยนตร์เปลี่ยนแปลง

และจริงๆ แล้วสิ่งนี้ทำได้โดยใช้เพียงบรรทัดเดียว โดยมีมุมมองที่เชื่อมโยงกับรัฐช่วย หากคุณเคยทำงานกับ ReSwift บน iOS หรือแม้แต่ ต่อ ด้วย React คุณจะเข้าใจความมหัศจรรย์ของ SwiftUI

ตอนนี้คุณสามารถลองเปิดใช้งานการดำเนินการและเผยแพร่สถานะใหม่ได้ นี่เป็นตัวอย่างที่ซับซ้อนมากขึ้น

struct CustomListDetail : View {
    @EnvironmentObject var store: Store<AppState>
 
    let listId: Int
    
    var list: CustomList {
        store.state.moviesState.customLists[listId]!
    }
    
    var movies: [Int] {
        list.movies.sortedMoviesIds(by: .byReleaseDate, state: store.state)
    }
    
    var body: some View {
        List {
            ForEach(movies) { movie in
                NavigationLink(destination: MovieDetail(movieId: movie).environmentObject(self.store)) {
                    MovieRow(movieId: movie, displayListImage: false)
                }
            }.onDelete { (index) in
               self.store.dispatch(action: MoviesActions.RemoveMovieFromCustomList(list: self.listId, movie: self.movies[index.first!]))
            }
        }
    }
}

ในโค้ดด้านบน ฉันใช้การดำเนินการ .onDelete จาก SwiftUI สำหรับแต่ละ IP ซึ่งจะทำให้แถวในรายการแสดงการปัด iOS ปกติเพื่อลบ ดังนั้นเมื่อผู้ใช้แตะปุ่มลบ ระบบจะเรียกใช้การกระทำที่เกี่ยวข้องและลบภาพยนตร์ออกจากรายการ

เนื่องจากคุณสมบัติของรายการนั้นได้มาจากสถานะ BindableObject และถูกแทรกเป็น EnvironmentObject ดังนั้น SwiftUI จึงอัปเดตรายการเนื่องจาก ForEach เชื่อมโยงกับคุณสมบัติที่คำนวณของภาพยนตร์

นี่คือส่วนหนึ่งของตัวลด MoviesState:

func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
    var state = state
    switch action {
    
    // other actions.
    
    case let action as MoviesActions.AddMovieToCustomList:
        state.customLists[action.list]?.movies.append(action.movie)
        
    case let action as MoviesActions.RemoveMovieFromCustomList:
        state.customLists[action.list]?.movies.removeAll{ $0 == action.movie }
        
    default:
        break
    }
    return state
}

ตัวลดจะถูกดำเนินการเมื่อคุณส่งการดำเนินการและส่งคืนสถานะใหม่ตามที่ระบุไว้ข้างต้น

ฉันจะยังไม่ลงรายละเอียด - SwiftUI รู้ได้อย่างไรว่าจะแสดงอะไร เพื่อให้เข้าใจสิ่งนี้อย่างลึกซึ้งยิ่งขึ้นมันก็คุ้มค่า ดูเซสชัน WWDC เกี่ยวกับการไหลของข้อมูล ใน SwiftUI นอกจากนี้ยังอธิบายรายละเอียดว่าทำไมจึงควรใช้และเมื่อใด สถานะ, @Binding, ObjectBinding และ EnvironmentObject

Skillbox แนะนำ:

ที่มา: will.com

เพิ่มความคิดเห็น