【问题标题】:Swift: Retrieve value from asynchronous call before view appearsSwift:在视图出现之前从异步调用中检索值
【发布时间】:2017-01-12 09:04:46
【问题描述】:

我正在使用 HanekeSwift 检索缓存的数据,然后在每次视图出现时将其设置为 swipeView 中的标签。我的代码检索数据没有问题,但是因为cache.fetch()是异步的,所以当我调用我的方法更新视图时,我的标签设置为nil。无论如何要告诉 swift 在加载视图之前等到我的缓存数据被检索到?

见下面的代码:

override func viewWillAppear(animated: Bool) {
    updateEntries() // updates entries from cache when view appears
}

func updateEntries() {
    guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
    guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }
    cache.fetch(key: cachedEntryKey).onSuccess { data in
        ...
        // if successful, set labels in swipeView to data retrieved from cache
        ...
        dispatch_group_leave(dispatchGroup)
    } .onFailure { error in
        print(error)
        ...
        // if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
        ...
        dispatch_group_leave(dispatchGroup)
    }
}

当我单步执行上述代码时,它总是显示视图,然后单步进入缓存块。如何使 viewWillAppear() 允许 updateEntries() 完成并且在执行缓存块之前不返回它?提前致谢!

更新 1:

下面的解决方案运行良好,我的调用以正确的顺序进行(我在通知块中的打印语句在缓存检索后执行),但我的视图仅在服务器运行时使用非零值更新它们的标签叫。也许我在通知组中混入了错误的代码?

override func viewWillAppear(animated: Bool) {
    self.addProgressHUD()
    updateEntries() // updates entries from cache when view appears
}

func updateEntries() {
    guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
    guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }

    let dispatchGroup = dispatch_group_create()
    dispatch_group_enter(dispatchGroup)

    dispatch_group_async(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
        cache.fetch(key: cachedEntryKey).onSuccess { data in
            ...
            // if successful, set labels in swipeView to data retrieved from cache
            ...
        } .onFailure { error in
            print(error)
            ...
            // if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
            ...
        }
    }

    dispatch_group_notify(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
        print("Retrieved Data")
        self.removeProgressHUD()
    }

}

更新 2:

另外,当我切换视图时,我会在控制台中收到此警告。我想我用上面的代码锁定了主线程

“此应用程序正在从后台线程修改自动布局引擎,这可能导致引擎损坏和奇怪的崩溃。这将在未来的版本中导致异常。”

【问题讨论】:

  • 你不应该阻塞主线程,因为它负责将 UI 组件绘制到屏幕上并处理用户与它们的交互。您应该让您的 UI 功能齐全,并在没有数据时看起来正常。
  • @ozgur 明白了。那么这是否意味着一旦我检索到该数据,那么我应该刷新视图?我尝试在我的 swipeView 中重新加载数据,但似乎没有解决问题
  • 是的,如果你的 UI 依赖于显示来自远程资源的一些内容,基本流程是这样的:你通过显示一些加载指示器来禁用用户交互,调用检索内容,然后更新您的数据源控制内容并重新加载 UI。
  • @ozgur 好的,那么我将如何设置加载屏幕以等待按此顺序检索数据 1. viewLoads 和加载屏幕出现 2. 加载屏幕出现 3. 缓存或服务器被调用检索数据 4. 视图设置为值 5. 加载屏幕被移除 目前,加载屏幕不会等到数据被检索到才会被移除,因为事情是异步发生的
  • 在类似的问题中,我读到我需要使用完成处理程序来允许显示数据。这个问题是不是这样,如果是这样,有人可以提供一个如何解决这个问题的例子吗?

标签: swift asynchronous closures grand-central-dispatch haneke


【解决方案1】:

每个人的好建议对此都有很大帮助。想我明白了。我需要确保我的缓存块没有阻塞主队列。见下面的代码

编辑

感谢 @Rob 帮助我进行适当的调整以完成这项工作

let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup)

cache.fetch(key: cachedEntryKey).onSuccess { data in
    ...
    // if successful, set labels in swipeView to data retrieved from cache
    ...
    dispatch_group_leave(dispatchGroup)
} .onFailure { error in
    print(error)
    ...
    // if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
    ...
    dispatch_group_leave(dispatchGroup)
}

dispatch_group_notify(dispatchGroup, dispatch_get_main_queue()) {
    print("Retrieved Data")
    self.removeProgressHUD()
}

【讨论】:

    【解决方案2】:

    注意:

    • 在调用异步方法之前进入组
    • 离开组是各自的完成/失败处理程序
    • 将通知块中的 UI 更新发送到 队列

    因此:

    func updateEntries() {
        guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
        guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }
    
        let group = dispatch_group_create()
        dispatch_group_enter(group)
    
        cache.fetch(key: cachedEntryKey).onSuccess { data in
            ...
            // if successful, set labels in swipeView to data retrieved from cache
            ...
            dispatch_group_leave(group)
        } .onFailure { error in
            print(error)
            ...
            // if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
            ...
            dispatch_group_leave(group)
        }
    
        dispatch_group_notify(group, dispatch_get_main_queue()) {
            print("Retrieved Data")
            self.removeProgressHUD()
        }
    
    }
    

    【讨论】:

    • 谢谢@Rob 我已经更新了我的答案以反映您的建议。现在我遇到的问题是,当我在同一设备上登录和注销不同帐户时,我不断收到“给定令牌无效”的 Alamofire 错误。我认为它可能会在将 accessToken 设置为viewDidLoad() 之前尝试使用它。你认为这也是一个异步问题吗?
    • 好的,当我看到调试器时,我会通过这个自言自语。发生的事情是 cahce 块在运行之前不会等待 accessToken 设置,因此它不是将用户踢出并进入登录屏幕,而是在有机会触发之前加载带有 nil 值的主屏幕错误并显示登录屏幕。那么我是否应该要求在缓存和其他所有事情之前检查有效令牌,如果是这样,我会使用调度组来实现吗?
    • 对不起,我对平台不熟悉!我会编辑并给你信用
    • 这个令牌的东西听起来像是一个新问题,但听起来像是一个逻辑流问题,而不是调度组的事情。但是根据您的描述,很难准确理解。但听起来你有一个可行的理论!不过,至少,我会确保(a)在用户注销时删除旧令牌; (b) 如果您在令牌为nil 时发出请求,请添加一些错误处理。这样,您至少能够正确识别问题,而不是发送带有无效令牌的请求。
    • 听起来不错。如果有问题,我会继续调试并发布一个新问题。非常感谢您的帮助!
    【解决方案3】:

    虽然我完全同意 @ozgur 关于从用户体验的角度显示某种加载指示器的观点,但我认为学习如何使用 Grand Central Dispatch(Apple 的异步等待的本机解决方案)的好处可能会从长远来看对您有所帮助.


    您可以使用dispatch_groups 等待代码块完全完成运行,然后再运行某种完成处理程序。

    来自Apple's documentation

    调度组是一种监控一组块的机制。您的应用程序可以根据您的需要同步或异步监视组中的块。通过扩展,组可用于同步依赖于其他任务完成的代码。

    [...]

    调度组跟踪有多少块未完成,GCD 保留该组,直到其所有关联的块完成执行。

    下面是dispatch_groups 的示例:

    let dispatchGroup = dispatch_group_create()
    
    dispatch_group_async(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
        // Run whatever code you need to in here. It will only move to the final
        // dispatch_group_notify block once it reaches the end of the block.
    }
    
    dispatch_group_notify(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
        // Code in here only runs once all dispatch_group_async blocks associated
        // with the dispatchGroup have finished completely.
    }
    

    dispatch_groups 的重要之处在于,它们允许您同时运行多个异步块,并等待它们全部完成,然后再运行最终的完成处理程序。换句话说,您可以将任意数量的dispatch_group_async 块与dispatchGroup 关联起来。

    如果您想采用加载指示器方法(您应该这样做),您可以运行代码来显示加载指示器,然后移动到带有完成处理程序的 dispatch_group 以删除加载指示器并将数据加载到视图中一旦dispatch_group 完成。

    【讨论】:

    • 这真的很有帮助。谢谢!因此,对于我的问题,我尝试调用 dispatch_group_async 块中的 updateEntries() 和 dispatch_group_notify 块中的一些其他代码,但 notify 块中的代码仍然首先执行。这是因为我在 updateEntries() 中有异步调用。我的猜测是我必须将我的 cache.fetch() 调用放在 dispatch_group_async 块中。是还是不是?
    • 是的!这解决了它!非常感谢!
    • 实际上,这只是在我调用 API 检索数据时修复它:/。当它从缓存中拉出时,似乎在完成其他所有操作之后仍然可以完成
    • @ObiAnachebe - 是的,如果您调用的代码本身是异步的,则此 dispatch_group_async 将不起作用。在这种情况下,您在调用异步代码之前执行dispatch_group_enter,在异步方法的完成处理程序中执行dispatch_group_leave,然后dispatch_group_notify 将按照Zachary 的说明工作。
    • @Rob 非常感谢。当我从缓存中检索数据时,它似乎运行良好。看我的更新。感觉我真的很亲近
    【解决方案4】:

    这是一个简单的示例,您可以设置一个加载屏幕。我只是创建一个警报视图,您也可以创建自定义加载指示器视图

    let alert = UIAlertController(title: "", message: "please wait ...", preferredStyle: .alert)
    
    override func viewWillAppear(animated: Bool) {
        self.present(alert, animated: true, completion: nil)
        updateEntries() // updates entries from cache when view appears
    }
    
    func updateEntries() {
        guard let accessToken = UserDefaults.standard.value(forKey: "accessToken") as? String,
              let cachedEntryKey = (accessToken + "food_entries.get") as? String else { 
          return 
        }
        cache.fetch(key: cachedEntryKey).onSuccess { data in
            ...
                 // update value in your UI
                 alert.dismiss(animated: true, completion: nil)
            ...
            } .onFailure { error in
                print(error)
                ...
                    // if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
                ...
        }
    }
    

    【讨论】:

    • 我用我为视图创建的progressHUD() 函数尝试了这个,虽然它一直等到异步调用完成后才移除HUD,但它仍会使用nil 值加载视图。我是否应该让加载屏幕成为一个单独的视图并显示缓存正在获取数据时,然后删除该加载视图?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多