首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >SwiftUI:@StateObject的未发布属性导致视图重绘

SwiftUI:@StateObject的未发布属性导致视图重绘
EN

Stack Overflow用户
提问于 2022-08-08 15:50:18
回答 1查看 165关注 0票数 1

在苹果的SwiftUI教程“‘Scrumdinger”中,ScrumTimer模型发布了三个属性,而speakers属性不包括在内。

代码语言:javascript
复制
class ScrumTimer: ObservableObject {

    /// The name of the meeting attendee who is speaking.

    @Published var activeSpeaker = ""

    /// The number of seconds since the beginning of the meeting.

    @Published var secondsElapsed = 0

    /// The number of seconds until all attendees have had a turn to speak.

    @Published var secondsRemaining = 0

    /// NOTE: This property is not published.

    private(set) var speakers: [Speaker] = []
}

但是,当我们单击MeetingFooterView中的“Next”按钮,它将更新scrumTimer.speakers数组和其他已发布的scrumTimer属性时,MeetingFooterView将被重新绘制。

ScrumTimer:

代码语言:javascript
复制
/*
See LICENSE folder for this sample’s licensing information.
*/

import Foundation

/// Keeps time for a daily scrum meeting. Keep track of the total meeting time, the time for each speaker, and the name of the current speaker.
class ScrumTimer: ObservableObject {
    /// A struct to keep track of meeting attendees during a meeting.
    struct Speaker: Identifiable {
        /// The attendee name.
        let name: String
        /// True if the attendee has completed their turn to speak.
        var isCompleted: Bool
        /// Id for Identifiable conformance.
        let id = UUID()
    }
    
    /// The name of the meeting attendee who is speaking.
    @Published var activeSpeaker = ""
    /// The number of seconds since the beginning of the meeting.
   @Published var secondsElapsed = 0
    /// The number of seconds until all attendees have had a turn to speak.
    @Published var secondsRemaining = 0
    /// All meeting attendees, listed in the order they will speak.
    private(set) var speakers: [Speaker] = []

    /// The scrum meeting length.
    private(set) var lengthInMinutes: Int
    /// A closure that is executed when a new attendee begins speaking.
    var speakerChangedAction: (() -> Void)?

    private var timer: Timer?
    private var timerStopped = false
    private var frequency: TimeInterval { 1.0 / 60.0 }
    private var lengthInSeconds: Int { lengthInMinutes * 60 }
    private var secondsPerSpeaker: Int {
        (lengthInMinutes * 60) / speakers.count
    }
    private var secondsElapsedForSpeaker: Int = 0
    private var speakerIndex: Int = 0
    private var speakerText: String {
        return "Speaker \(speakerIndex + 1): " + speakers[speakerIndex].name
    }
    private var startDate: Date?
    
    /**
     Initialize a new timer. Initializing a time with no arguments creates a ScrumTimer with no attendees and zero length.
     Use `startScrum()` to start the timer.
     
     - Parameters:
        - lengthInMinutes: The meeting length.
        -  attendees: A list of attendees for the meeting.
     */
    init(lengthInMinutes: Int = 0, attendees: [DailyScrum.Attendee] = []) {
        self.lengthInMinutes = lengthInMinutes
        self.speakers = attendees.speakers
        secondsRemaining = lengthInSeconds
        activeSpeaker = speakerText
    }
    
    /// Start the timer.
    func startScrum() {
        changeToSpeaker(at: 0)
    }
    
    /// Stop the timer.
    func stopScrum() {
        timer?.invalidate()
        timer = nil
        timerStopped = true
    }
    
    /// Advance the timer to the next speaker.
    func skipSpeaker() {
        changeToSpeaker(at: speakerIndex + 1)
    }

    private func changeToSpeaker(at index: Int) {
        if index > 0 {
            let previousSpeakerIndex = index - 1
            speakers[previousSpeakerIndex].isCompleted = true
        }
        secondsElapsedForSpeaker = 0
        guard index < speakers.count else { return }
        speakerIndex = index
        activeSpeaker = speakerText

        secondsElapsed = index * secondsPerSpeaker
        secondsRemaining = lengthInSeconds - secondsElapsed
        startDate = Date()
        timer = Timer.scheduledTimer(withTimeInterval: frequency, repeats: true) { [weak self] timer in
            if let self = self, let startDate = self.startDate {
                let secondsElapsed = Date().timeIntervalSince1970 - startDate.timeIntervalSince1970
                self.update(secondsElapsed: Int(secondsElapsed))
            }
        }
    }

    private func update(secondsElapsed: Int) {
        secondsElapsedForSpeaker = secondsElapsed
        self.secondsElapsed = secondsPerSpeaker * speakerIndex + secondsElapsedForSpeaker
        guard secondsElapsed <= secondsPerSpeaker else {
            return
        }
        secondsRemaining = max(lengthInSeconds - self.secondsElapsed, 0)

        guard !timerStopped else { return }

        if secondsElapsedForSpeaker >= secondsPerSpeaker {
            changeToSpeaker(at: speakerIndex + 1)
            speakerChangedAction?()
        }
    }
    
    /**
     Reset the timer with a new meeting length and new attendees.
     
     - Parameters:
         - lengthInMinutes: The meeting length.
         - attendees: The name of each attendee.
     */
    func reset(lengthInMinutes: Int, attendees: [DailyScrum.Attendee]) {
        self.lengthInMinutes = lengthInMinutes
        self.speakers = attendees.speakers
        secondsRemaining = lengthInSeconds
        activeSpeaker = speakerText
        print("## reset speakers: \(speakers)")
        print("## reset lengthInMinites: \(lengthInMinutes)")
        print("## reset secondsRemaining: \(secondsRemaining)")
        print("## reset activeSpeaker: \(activeSpeaker)")
    }
}

extension DailyScrum {
    /// A new `ScrumTimer` using the meeting length and attendees in the `DailyScrum`.
    var timer: ScrumTimer {
        ScrumTimer(lengthInMinutes: lengthInMinutes, attendees: attendees)
    }
}

extension Array where Element == DailyScrum.Attendee {
    var speakers: [ScrumTimer.Speaker] {
        if isEmpty {
            return [ScrumTimer.Speaker(name: "Speaker 1", isCompleted: false)]
        } else {
            return map { ScrumTimer.Speaker(name: $0.name, isCompleted: false) }
        }
    }
}

MeetingFooterView:

代码语言:javascript
复制
struct MeetingFooterView: View {
    let speakers: [ScrumTimer.Speaker]
    var skipAction: ()->Void
    
    private var speakerNumber: Int? {
        guard let index = speakers.firstIndex(where: { !$0.isCompleted }) else { return nil}
        return index + 1
    }
    private var isLastSpeaker: Bool {
        print("### MeetingFooterView updated!")
        print("### speakers is: \(speakers)")
        return speakers.dropLast().allSatisfy { $0.isCompleted }
    }
    private var speakerText: String {
        guard let speakerNumber = speakerNumber else { return "No more speakers" }
        return "Speaker \(speakerNumber) of \(speakers.count)"
    }
    
    var body: some View {
        if #available(iOS 15.0, *) {
            Self._printChanges()
        }
        
        return VStack {
            HStack {
                if isLastSpeaker {
                    Text("Last Speaker")
                } else {
                    Text(speakerText)
                    Spacer()
                    Button(action: skipAction) {
                        Image(systemName: "forward.fill")
                    }
                    .accessibilityLabel("Next speaker")
                }
            }
        }
        .padding([.bottom, .horizontal])
    }
}

MeetingView:

代码语言:javascript
复制
struct MeetingView: View {

    @Binding var scrum: DailyScrum

    @StateObject var scrumTimer = ScrumTimer()

    

    private var player: AVPlayer { AVPlayer.sharedDingPlayer }

    

    var body: some View {

        ZStack {

            RoundedRectangle(cornerRadius: 16)

                .fill(scrum.theme.mainColor)

            VStack {

                MeetingHeaderView(secondsElapsed: scrumTimer.secondsElapsed, secondsRemaining: scrumTimer.secondsRemaining, theme: scrum.theme)

                Circle()

                    .strokeBorder(lineWidth: 24)

                MeetingFooterView(speakers: scrumTimer.speakers, skipAction: scrumTimer.skipSpeaker)

//              DebugView(speakers: scrumTimer.speakers)

            }

        }

        .padding()

        .foregroundColor(scrum.theme.accentColor)

        .onAppear {

            print("### .onAppear() run")

            scrumTimer.reset(lengthInMinutes: scrum.lengthInMinutes, attendees: scrum.attendees)

            scrumTimer.speakerChangedAction = {

                player.seek(to: .zero)

                player.play()

            }

            scrumTimer.startScrum()

        }

        .onDisappear {

            scrumTimer.stopScrum()

        }

        .navigationBarTitleDisplayMode(.inline)

    }

}

问题是:MeetingFooterView只依赖于scrumTimer.speakers,这是一个未发布的属性,它是如何根据scrumTimer.speakers是否已经改变而决定重新绘制的?我的意思是,scrumTimer.speakers是@StateObject模型的一个未发布的属性,如果它的值发生了变化,视图应该根据它重新绘制吗?

完整的代码可以获取这里。

谢谢。

=====更新=====

今天,当我关注同一个教程项目和下一章时,有一个关于ObservableObject的注释:“ObservableObject包括一个objectWillChange发布者,当它的一个@Published属性即将改变时,它就会发出。当scrums值发生变化时,任何观察ScrumStore实例的视图都会再次呈现出来。”既然@StateObject's rawValue 就是 an ObservableObject,那么它们的工作方式应该是一样的。

作为一个初学SwiftUI的人,我的困惑是@StateObject是在属性级别还是在对象级别作为一个整体发布其值。我首先认为@StateObject在属性级别发布其更改,因此发布的var secondsElapsed值更改不应导致MeetingFooterView依赖于未发布的var speakers属性重绘,即使speakers值已经更改。但这是不对的。正如上面提到的那样,“ObservableObject包括an(单一) objectWillChange publisher,当one@Published属性即将改变时,它就会发出”,这意味着@StateObject将其更改作为一个整体对象传递。

有了这种理解,我才知道为什么speakers属性不必是@Published。当程序运行时,ScumTimersecondsElapsed属性每秒钟更新一次,计时器也会更改speakers值if secondsElapsedForSpeaker >= secondsPerSpeaker,例如每100秒钟就会发生一次。因此,即使speakers属性没有发布,它的值变化也将每秒钟进行一次评估,因为secondsElapsed每秒钟都会发生变化,@StateObject会将其值更改作为一个整体进行传递。

但是,如果我们像下面这样修改它的属性,只发布speakers属性,

代码语言:javascript
复制
    /// The name of the meeting attendee who is speaking.
     var activeSpeaker = ""
    /// The number of seconds since the beginning of the meeting.
     var secondsElapsed = 0
    /// The number of seconds until all attendees have had a turn to speak.
     var secondsRemaining = 0
    /// All meeting attendees, listed in the order they will speak.
    @Published var speakers: [Speaker] = []

事情不会成功的。secondsElapsed仍然每秒钟更新一次,但是由于它没有发布,scrumTimer不会每秒钟传递一次更改。只有当speaker的值发生变化时,例如当我们单击"Next“按钮时,它才会传递更改。

EN

回答 1

Stack Overflow用户

发布于 2022-08-10 09:54:09

MeetingFooterView的主体被调用两次的原因是扬声器数组发生了变化。这是因为ScrumTimer首先是不带attendees的,当attendees.speakers被称为isEmpty时,检查结果是设置一个初始的默认扬声器,然后在onAppear中设置实际的参与者,从而改变扬声器。此外,如果没有实际的参与者,那么就再次创建一个新的初始默认扬声器,因此即使在这种情况下,扬声器数组仍然被更改。

代码语言:javascript
复制
MeetingFooterView: @self changed.
Printing description of self.speakers:
▿ 1 element
  ▿ 0 : Speaker
    - name : "Speaker 1"
    - isCompleted : false
    ▿ id : 220F9930-E774-4A16-B62F-BF45C0100FDB

MeetingFooterView: @self changed.
Printing description of self.speakers:
▿ 1 element
  ▿ 0 : Speaker
    - name : "John"
    - isCompleted : false
    ▿ id : C53672E2-5043-4364-81E0-C9D44ACDF8CF

printChanges应该向self.speakers changed表明,不知道为什么没有。

您可以通过删除ScrumTimer init方法并将var lengthInMinutes默认为0来整理这一点。还请注意,DailyScum在扩展中有2个timer vars:var timer: Timer?var timer: ScrumTimer。首先,移除扩展部分。然后在onDissapear中,当scrum停止时:

代码语言:javascript
复制
let newHistory = History(attendees: scrum.attendees, lengthInMinutes: scrum.timer.secondsElapsed / 60)

这个对scrum.timer的调用创建了一个新的ScrumTimer对象!

应:

代码语言:javascript
复制
let newHistory = History(attendees: scrum.attendees, lengthInMinutes: scrumTimer.secondsElapsed / 60)

这个示例可能有更多的问题,请小心使用它!

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/73280727

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档