Files
suixinkan_uikit/suixinkan/UI/Profile/ProfileSpaceSettingsViewController.swift
汉秋 cbf2db649e 完善素材管理 UI 交互并修复列表解析与时间选择。
MaterialItem 兼容 Android 缺失字段默认值,优化素材详情/列表/标签布局与上下架开关交互,修复个人空间时间选择器初始值。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 13:34:45 +08:00

1384 lines
55 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ProfileSpaceSettingsViewController.swift
// suixinkan
//
import Kingfisher
import PhotosUI
import SnapKit
import UIKit
/// Android `ProfileInfoScreen`
final class ProfileSpaceSettingsViewController: BaseViewController {
private let viewModel: ProfileSpaceSettingsViewModel
private let profileAPI: ProfileAPI
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
private let bottomBar = UIView()
private let saveButton = AppButton(title: "保存")
private let avatarImageView = UIImageView()
private let avatarChangeOverlay = UIView()
private let avatarChangeLabel = UILabel()
private let realNameLabel = UILabel()
private let nicknameField = UITextField()
private let editProfileButton = UIButton(type: .system)
private let introductionTextView = UITextView()
private let introductionPlaceholderLabel = UILabel()
private let certificationWrap = ProfileTagWrapView()
private let certificationExpandButton = UIButton(type: .system)
private let attrWrap = ProfileTagWrapView()
private let shootWrap = ProfileTagWrapView()
private let attrField = UITextField()
private let shootField = UITextField()
private let attrAddButton = UIButton(type: .system)
private let shootAddButton = UIButton(type: .system)
private let businessStartButton = ProfileTimeButton()
private let businessEndButton = ProfileTimeButton()
private let holidayStartButton = ProfileTimeButton()
private let holidayEndButton = ProfileTimeButton()
private let deviceWrap = ProfileTagWrapView()
private let addDeviceButton = ProfileDashedBorderButton(type: .system)
private var statusButtons: [UIButton] = []
private let dateStack = UIStackView()
private let selectedDateLabel = UILabel()
private let scheduleStack = UIStackView()
private let addScheduleButton = UIButton(type: .system)
init(
viewModel: ProfileSpaceSettingsViewModel = ProfileSpaceSettingsViewModel(),
profileAPI: ProfileAPI = NetworkServices.shared.profileAPI
) {
self.viewModel = viewModel
self.profileAPI = profileAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "个人空间配置"
navigationItem.backButtonDisplayMode = .minimal
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
ProfileNavigationStyle.apply(to: navigationController, showsDivider: false)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
ProfileNavigationStyle.restore(navigationController)
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackground
scrollView.alwaysBounceVertical = true
view.addSubview(scrollView)
view.addSubview(bottomBar)
scrollView.addSubview(contentStack)
bottomBar.addSubview(saveButton)
contentStack.axis = .vertical
contentStack.spacing = 16
contentStack.layoutMargins = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
contentStack.isLayoutMarginsRelativeArrangement = true
bottomBar.backgroundColor = .white
saveButton.isEnabled = false
saveButton.snp.updateConstraints { make in make.height.equalTo(48) }
configureProfileCard()
configureLabelCard()
configureBusinessHoursCard()
configureDeviceCard()
configureOrderStatusCard()
configureScheduleCard()
}
override func setupConstraints() {
bottomBar.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
}
saveButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.leading.trailing.equalToSuperview().inset(15)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
}
scrollView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.bottom.equalTo(bottomBar.snp.top)
}
contentStack.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.width.equalTo(scrollView.snp.width)
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
editProfileButton.addTarget(self, action: #selector(editProfileTapped), for: .touchUpInside)
certificationExpandButton.addTarget(self, action: #selector(certificationExpandTapped), for: .touchUpInside)
addDeviceButton.addTarget(self, action: #selector(addDeviceTapped), for: .touchUpInside)
addScheduleButton.addTarget(self, action: #selector(addScheduleTapped), for: .touchUpInside)
attrField.addTarget(self, action: #selector(labelInputChanged), for: .editingChanged)
shootField.addTarget(self, action: #selector(labelInputChanged), for: .editingChanged)
[businessStartButton, businessEndButton, holidayStartButton, holidayEndButton].forEach {
$0.addTarget(self, action: #selector(timeButtonTapped(_:)), for: .touchUpInside)
}
let avatarTap = UITapGestureRecognizer(target: self, action: #selector(avatarTapped))
avatarImageView.addGestureRecognizer(avatarTap)
avatarImageView.isUserInteractionEnabled = true
}
override func viewDidLoad() {
super.viewDidLoad()
Task { await viewModel.load(api: profileAPI) }
}
@MainActor
private func applyViewModel() {
if viewModel.isLoading {
showLoading()
} else {
hideLoading()
}
saveButton.isEnabled = viewModel.hasChanges
updateAvatar(urlString: viewModel.avatarURL)
realNameLabel.text = "姓名: \(viewModel.realName)"
nicknameField.text = viewModel.isEditingProfile ? viewModel.editingNickname : "昵称: \(viewModel.nickname)"
nicknameField.isEnabled = viewModel.isEditingProfile
introductionTextView.isEditable = viewModel.isEditingProfile
introductionTextView.text = viewModel.introduction.isEmpty && !viewModel.isEditingProfile ? "暂无简介" : viewModel.introduction
introductionTextView.textColor = viewModel.introduction.isEmpty && !viewModel.isEditingProfile ? UIColor(hex: 0xB6BECA) : UIColor(hex: 0x4B5563)
updateIntroductionPlaceholder()
editProfileButton.setImage(ProfileSpaceIcon.edit, for: .normal)
avatarChangeOverlay.isHidden = !viewModel.isEditingProfile
let visibleCertifications = viewModel.isCertificationExpanded
? viewModel.scenicCertification
: Array(viewModel.scenicCertification.prefix(3))
certificationWrap.setItems(visibleCertifications, style: .certification, removable: false)
certificationExpandButton.isHidden = viewModel.scenicCertification.count <= 3
certificationExpandButton.setTitle(viewModel.isCertificationExpanded ? "收起" : "展开", for: .normal)
attrWrap.setItems(viewModel.attrLabels, style: .label, removable: true) { [weak self] label in
self?.confirmDelete(title: "删除标签", message: "您确定要删除\(label)标签吗?") {
self?.viewModel.removeAttrLabel(label)
}
}
shootWrap.setItems(viewModel.shootLabels, style: .label, removable: true) { [weak self] label in
self?.confirmDelete(title: "删除拍摄说明", message: "您确定要删除\(label)拍摄说明吗?") {
self?.viewModel.removeShootLabel(label)
}
}
businessStartButton.setTitle(viewModel.businessStartTime ?? "00:00", for: .normal)
businessEndButton.setTitle(viewModel.businessEndTime ?? "00:00", for: .normal)
holidayStartButton.setTitle(viewModel.holidayStartTime ?? "00:00", for: .normal)
holidayEndButton.setTitle(viewModel.holidayEndTime ?? "00:00", for: .normal)
deviceWrap.setItems(viewModel.cameraDevices, style: .device, removable: true) { [weak self] device in
self?.confirmDelete(title: "删除设备", message: "您确定要删除\(device)设备吗?") {
self?.viewModel.removeDevice(device)
}
}
addDeviceButton.isHidden = viewModel.cameraDevices.count > 10
deviceWrap.invalidateIntrinsicContentSize()
deviceWrap.setNeedsLayout()
updateStatusButtons()
updateLabelAddButtons()
rebuildDateButtons()
rebuildSchedules()
}
private func configureProfileCard() {
let card = makeCard()
let row = UIStackView()
row.axis = .horizontal
row.alignment = .top
row.spacing = 16
avatarImageView.layer.cornerRadius = 58
avatarImageView.clipsToBounds = true
avatarImageView.backgroundColor = AppColor.primaryLight
avatarImageView.contentMode = .scaleAspectFill
avatarImageView.image = UIImage(named: "profile_avatar_default")
avatarImageView.tintColor = UIColor(hex: 0xB6BECA)
avatarImageView.accessibilityLabel = "头像"
avatarImageView.snp.makeConstraints { make in
make.width.height.equalTo(116)
}
avatarImageView.addSubview(avatarChangeOverlay)
avatarChangeOverlay.backgroundColor = UIColor.black.withAlphaComponent(0.35)
avatarChangeOverlay.isHidden = true
avatarChangeOverlay.addSubview(avatarChangeLabel)
avatarChangeLabel.text = "点击更换"
avatarChangeLabel.textColor = .white
avatarChangeLabel.font = .systemFont(ofSize: 12, weight: .medium)
avatarChangeOverlay.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
avatarChangeLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
let textStack = UIStackView()
textStack.axis = .vertical
textStack.spacing = 0
realNameLabel.font = .systemFont(ofSize: 14)
realNameLabel.textColor = UIColor(hex: 0x4B5563)
let nicknameRow = UIStackView()
nicknameRow.axis = .horizontal
nicknameRow.alignment = .center
nicknameRow.spacing = 8
nicknameField.font = .systemFont(ofSize: 16)
nicknameField.textColor = .black
nicknameField.borderStyle = .none
nicknameField.placeholder = "请输入昵称"
nicknameField.returnKeyType = .done
nicknameField.delegate = self
nicknameField.addTarget(self, action: #selector(nicknameChanged), for: .editingChanged)
editProfileButton.tintColor = AppColor.primary
editProfileButton.accessibilityLabel = "编辑个人信息"
editProfileButton.snp.makeConstraints { make in
make.width.height.equalTo(44)
}
nicknameRow.addArrangedSubview(nicknameField)
nicknameRow.addArrangedSubview(editProfileButton)
introductionTextView.font = .systemFont(ofSize: 14)
introductionTextView.backgroundColor = .clear
introductionTextView.textContainerInset = .zero
introductionTextView.textContainer.lineFragmentPadding = 0
introductionTextView.delegate = self
introductionTextView.isScrollEnabled = false
introductionTextView.snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(24)
}
introductionPlaceholderLabel.text = "请输入个人简介"
introductionPlaceholderLabel.font = introductionTextView.font
introductionPlaceholderLabel.textColor = UIColor(hex: 0xB6BECA)
introductionTextView.addSubview(introductionPlaceholderLabel)
introductionPlaceholderLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
}
textStack.addArrangedSubview(realNameLabel)
textStack.addArrangedSubview(nicknameRow)
textStack.setCustomSpacing(8, after: nicknameRow)
textStack.addArrangedSubview(introductionTextView)
textStack.setCustomSpacing(4, after: introductionTextView)
textStack.addArrangedSubview(certificationWrap)
certificationExpandButton.setTitleColor(AppColor.primary, for: .normal)
certificationExpandButton.titleLabel?.font = .systemFont(ofSize: 14)
certificationExpandButton.contentHorizontalAlignment = .left
certificationExpandButton.isHidden = true
certificationExpandButton.snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(20)
}
textStack.addArrangedSubview(certificationExpandButton)
row.addArrangedSubview(avatarImageView)
row.addArrangedSubview(textStack)
card.addArrangedSubview(row)
contentStack.addArrangedSubview(card)
}
private func configureLabelCard() {
let card = makeCard()
card.spacing = 0
let title = makeSectionTitle("标签")
card.addArrangedSubview(title)
card.setCustomSpacing(12, after: title)
card.addArrangedSubview(attrWrap)
card.setCustomSpacing(8, after: attrWrap)
let attrRow = makeAddRow(
field: attrField,
button: attrAddButton,
placeholder: "添加标签",
action: #selector(addAttrTapped)
)
card.addArrangedSubview(attrRow)
card.setCustomSpacing(20, after: attrRow)
card.addArrangedSubview(shootWrap)
card.setCustomSpacing(12, after: shootWrap)
let shootRow = makeAddRow(
field: shootField,
button: shootAddButton,
placeholder: "添加拍摄说明",
action: #selector(addShootTapped)
)
card.addArrangedSubview(shootRow)
card.setCustomSpacing(8, after: shootRow)
let hint = UILabel()
hint.text = "每个标签最多20个字符,最多8个标签"
hint.font = .systemFont(ofSize: 12)
hint.textColor = UIColor(hex: 0xB6BECA)
card.addArrangedSubview(hint)
contentStack.addArrangedSubview(card)
}
private func configureBusinessHoursCard() {
let card = makeCard()
let title = makeSectionTitle("营业时间")
let workdayRow = makeTimeRow(title: "工作日营业时间", start: businessStartButton, end: businessEndButton)
card.addArrangedSubview(title)
card.addArrangedSubview(workdayRow)
card.setCustomSpacing(20, after: workdayRow)
card.addArrangedSubview(makeTimeRow(title: "法定节假日营业时间", start: holidayStartButton, end: holidayEndButton))
contentStack.addArrangedSubview(card)
}
private func configureDeviceCard() {
let card = makeCard()
card.addArrangedSubview(makeSectionTitle("相机设备"))
card.addArrangedSubview(deviceWrap)
addDeviceButton.setTitle(" 添加设备", for: .normal)
addDeviceButton.setTitleColor(AppColor.primary, for: .normal)
addDeviceButton.titleLabel?.font = .systemFont(ofSize: 14)
addDeviceButton.layer.cornerRadius = 4
addDeviceButton.contentEdgeInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
addDeviceButton.contentHorizontalAlignment = .left
addDeviceButton.snp.makeConstraints { make in make.height.equalTo(28) }
deviceWrap.setTrailingView(addDeviceButton)
contentStack.addArrangedSubview(card)
}
private func configureOrderStatusCard() {
let card = makeCard()
card.addArrangedSubview(makeSectionTitle("接单状态"))
let labels = ["可接单", "暂停接单", "已约满", "下线"]
for rowIndex in 0..<2 {
let row = UIStackView()
row.axis = .horizontal
row.spacing = 15
row.distribution = .fillEqually
for index in 0..<2 {
let status = rowIndex * 2 + index + 1
let button = UIButton(type: .system)
button.setTitle(labels[status - 1], for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14)
button.tag = status
button.layer.cornerRadius = 8
button.layer.borderWidth = 1
button.addTarget(self, action: #selector(statusTapped(_:)), for: .touchUpInside)
button.snp.makeConstraints { make in
make.height.equalTo(52)
}
statusButtons.append(button)
row.addArrangedSubview(button)
}
card.addArrangedSubview(row)
}
contentStack.addArrangedSubview(card)
}
private func configureScheduleCard() {
let card = makeCard()
card.addArrangedSubview(makeSectionTitle("日程安排"))
dateStack.axis = .horizontal
dateStack.spacing = 4
dateStack.distribution = .fillEqually
card.addArrangedSubview(dateStack)
let header = UIStackView()
header.axis = .horizontal
header.alignment = .center
header.spacing = 12
selectedDateLabel.font = .systemFont(ofSize: 14)
selectedDateLabel.textColor = UIColor(hex: 0x252525)
addScheduleButton.setTitle("添加日程", for: .normal)
addScheduleButton.setTitleColor(AppColor.primary, for: .normal)
addScheduleButton.titleLabel?.font = .systemFont(ofSize: 14)
header.addArrangedSubview(selectedDateLabel)
header.addArrangedSubview(UIView())
header.addArrangedSubview(addScheduleButton)
card.addArrangedSubview(header)
card.setCustomSpacing(8, after: header)
scheduleStack.axis = .vertical
scheduleStack.spacing = 12
card.addArrangedSubview(scheduleStack)
contentStack.addArrangedSubview(card)
}
private func makeCard() -> UIStackView {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 16
stack.backgroundColor = .white
stack.layer.cornerRadius = 12
stack.clipsToBounds = true
stack.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
stack.isLayoutMarginsRelativeArrangement = true
return stack
}
private func makeSectionTitle(_ title: String) -> UILabel {
let label = UILabel()
label.text = title
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = ["相机设备", "接单状态", "日程安排"].contains(title)
? UIColor(hex: 0x4B5563)
: UIColor(hex: 0x252525)
return label
}
private func makeAddRow(
field: UITextField,
button: UIButton,
placeholder: String,
action: Selector
) -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.spacing = 8
row.alignment = .fill
field.placeholder = placeholder
field.font = .systemFont(ofSize: 14)
field.returnKeyType = .done
field.delegate = self
field.layer.cornerRadius = 8
field.layer.borderColor = AppColor.border.cgColor
field.layer.borderWidth = 1
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
field.leftViewMode = .always
field.snp.makeConstraints { make in
make.height.equalTo(36)
}
button.setTitle("添加标签", for: .normal)
button.setTitleColor(.white, for: .normal)
button.setTitleColor(.white, for: .disabled)
button.backgroundColor = AppColor.primary
button.titleLabel?.font = .systemFont(ofSize: 14)
button.layer.cornerRadius = 4
button.addTarget(self, action: action, for: .touchUpInside)
button.isEnabled = false
button.snp.makeConstraints { make in
make.width.equalTo(80)
make.height.equalTo(36)
}
row.addArrangedSubview(field)
row.addArrangedSubview(button)
return row
}
private func makeTimeRow(title: String, start: ProfileTimeButton, end: ProfileTimeButton) -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 10
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 14)
titleLabel.textColor = UIColor(hex: 0x4B5563)
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
let toLabel = UILabel()
toLabel.text = ""
toLabel.font = .systemFont(ofSize: 14)
toLabel.textColor = UIColor(hex: 0x4B5563)
let spacer = UIView()
row.addArrangedSubview(titleLabel)
row.addArrangedSubview(spacer)
row.addArrangedSubview(start)
row.addArrangedSubview(toLabel)
row.addArrangedSubview(end)
[start, end].forEach { button in
button.snp.makeConstraints { make in
make.width.equalTo(60)
make.height.equalTo(36)
}
}
return row
}
private func updateAvatar(urlString: String?) {
let trimmed = urlString?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else {
avatarImageView.kf.cancelDownloadTask()
avatarImageView.backgroundColor = AppColor.primaryLight
avatarImageView.image = UIImage(named: "profile_avatar_default")
avatarImageView.tintColor = UIColor(hex: 0xB6BECA)
avatarImageView.contentMode = .scaleAspectFit
return
}
avatarImageView.contentMode = .scaleAspectFill
avatarImageView.loadRemoteImage(urlString: trimmed)
}
private func updateIntroductionPlaceholder() {
introductionPlaceholderLabel.isHidden = !viewModel.isEditingProfile || !introductionTextView.text.isEmpty
introductionTextView.layer.borderWidth = viewModel.isEditingProfile ? 1 : 0
introductionTextView.layer.borderColor = UIColor(hex: 0xEEEEEE).cgColor
introductionTextView.layer.cornerRadius = 4
introductionTextView.textContainerInset = viewModel.isEditingProfile
? UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
: .zero
introductionPlaceholderLabel.snp.remakeConstraints { make in
make.top.equalToSuperview().offset(viewModel.isEditingProfile ? 8 : 0)
make.leading.equalToSuperview().offset(viewModel.isEditingProfile ? 8 : 0)
make.trailing.equalToSuperview().inset(viewModel.isEditingProfile ? 8 : 0)
}
}
private func updateLabelAddButtons() {
let pairs = [(attrField, attrAddButton), (shootField, shootAddButton)]
for (field, button) in pairs {
let enabled = !(field.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
button.isEnabled = enabled
button.backgroundColor = enabled ? AppColor.primary : AppColor.buttonDisabled
}
}
private func updateStatusButtons() {
for button in statusButtons {
let selected = button.tag == viewModel.acceptOrderStatus
button.backgroundColor = selected ? AppColor.primary : .white
button.layer.borderColor = (selected ? AppColor.primary : UIColor(hex: 0xB6BECA)).cgColor
button.setTitleColor(selected ? .white : UIColor(hex: 0x4B5563), for: .normal)
button.accessibilityTraits = selected ? [.button, .selected] : .button
}
}
private func rebuildDateButtons() {
dateStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
for date in viewModel.weekDates {
let button = ProfileDateButton()
button.configure(
weekday: ProfileSpaceSettingsViewModel.displayWeekday(date),
day: ProfileSpaceSettingsViewModel.displayDay(date),
selected: Calendar.current.isDate(date, inSameDayAs: viewModel.selectedDate)
)
button.date = date
button.addTarget(self, action: #selector(dateTapped(_:)), for: .touchUpInside)
dateStack.addArrangedSubview(button)
}
selectedDateLabel.text = ProfileSpaceSettingsViewModel.displayDate(viewModel.selectedDate)
}
private func rebuildSchedules() {
scheduleStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
let schedules = viewModel.selectedDateSchedules
guard !schedules.isEmpty else {
let empty = UILabel()
empty.text = "该日无日程"
empty.textColor = AppColor.textTertiary
empty.font = .systemFont(ofSize: 14)
empty.textAlignment = .center
empty.snp.makeConstraints { make in
make.height.equalTo(76)
}
scheduleStack.addArrangedSubview(empty)
return
}
for schedule in schedules {
let item = ProfileScheduleItemView(schedule: schedule)
item.onDelete = { [weak self] in
self?.confirmDelete(title: "删除日程", message: "您确定要删除该日程吗?") {
guard let self else { return }
Task { await self.viewModel.deleteSchedule(id: schedule.id, api: self.profileAPI) }
}
}
item.onCall = { [weak self] phone in
self?.callPhone(phone)
}
scheduleStack.addArrangedSubview(item)
}
}
private func confirmDelete(title: String, message: String, onConfirm: @escaping () -> Void) {
let dialog = ProfileDeleteDialogViewController(title: title, message: message, onConfirm: onConfirm)
present(dialog, animated: false)
}
private func showTimePicker(initialTime: String?, onConfirm: @escaping (String) -> Void) {
let dialog = ProfileTimePickerDialogViewController(
initialDate: date(fromTime: initialTime ?? "00:00"),
onConfirm: { [weak self] date in
guard let self else { return }
onConfirm(timeString(from: date))
}
)
present(dialog, animated: false)
}
private func date(fromTime value: String) -> Date {
var components = DateComponents()
let pieces = value.split(separator: ":").compactMap { Int($0) }
components.hour = pieces.first ?? 0
components.minute = pieces.dropFirst().first ?? 0
return Calendar.current.date(from: components) ?? Date()
}
private func timeString(from date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm"
return formatter.string(from: date)
}
private func callPhone(_ phone: String) {
let digits = phone.trimmingCharacters(in: .whitespacesAndNewlines)
guard let url = URL(string: "tel://\(digits)"), UIApplication.shared.canOpenURL(url) else { return }
UIApplication.shared.open(url)
}
@objc private func saveTapped() {
view.endEditing(true)
Task { await viewModel.save(api: profileAPI) }
}
@objc private func editProfileTapped() {
view.endEditing(true)
Task { await viewModel.toggleProfileEditing(api: profileAPI) }
}
@objc private func nicknameChanged() {
viewModel.updateEditingNickname(nicknameField.text ?? "")
}
@objc private func addAttrTapped() {
viewModel.addAttrLabel(attrField.text ?? "")
attrField.text = ""
updateLabelAddButtons()
}
@objc private func addShootTapped() {
viewModel.addShootLabel(shootField.text ?? "")
shootField.text = ""
updateLabelAddButtons()
}
@objc private func labelInputChanged() {
updateLabelAddButtons()
}
@objc private func certificationExpandTapped() {
viewModel.toggleCertificationExpanded()
}
@objc private func timeButtonTapped(_ sender: ProfileTimeButton) {
if sender === businessStartButton {
showTimePicker(initialTime: viewModel.businessStartTime) { [weak self] in self?.viewModel.updateBusinessStartTime($0) }
} else if sender === businessEndButton {
showTimePicker(initialTime: viewModel.businessEndTime) { [weak self] in self?.viewModel.updateBusinessEndTime($0) }
} else if sender === holidayStartButton {
showTimePicker(initialTime: viewModel.holidayStartTime) { [weak self] in self?.viewModel.updateHolidayStartTime($0) }
} else {
showTimePicker(initialTime: viewModel.holidayEndTime) { [weak self] in self?.viewModel.updateHolidayEndTime($0) }
}
}
@objc private func addDeviceTapped() {
let dialog = ProfileDeviceDialogViewController { [weak self] value in
self?.viewModel.addDevice(value)
}
present(dialog, animated: false)
}
@objc private func statusTapped(_ sender: UIButton) {
viewModel.updateAcceptOrderStatus(sender.tag)
}
@objc private func dateTapped(_ sender: ProfileDateButton) {
guard let date = sender.date else { return }
viewModel.selectDate(date)
}
@objc private func addScheduleTapped() {
let controller = ProfileAddScheduleViewController(
selectedDate: viewModel.selectedDate,
viewModel: viewModel,
profileAPI: profileAPI
)
navigationController?.pushViewController(controller, animated: true)
}
@objc private func avatarTapped() {
guard viewModel.isEditingProfile else { return }
var configuration = PHPickerConfiguration(photoLibrary: .shared())
configuration.filter = .images
configuration.selectionLimit = 1
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true)
}
}
extension ProfileSpaceSettingsViewController: UITextFieldDelegate, UITextViewDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textField(
_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String
) -> Bool {
guard textField === attrField || textField === shootField else { return true }
let current = textField.text ?? ""
guard let swiftRange = Range(range, in: current) else { return false }
return current.replacingCharacters(in: swiftRange, with: string).count <= 20
}
func textViewDidChange(_ textView: UITextView) {
viewModel.updateIntroduction(textView.text)
updateIntroductionPlaceholder()
}
func textView(
_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText text: String
) -> Bool {
guard text == "\n" else { return true }
textView.resignFirstResponder()
return false
}
}
extension ProfileSpaceSettingsViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let provider = results.first?.itemProvider else { return }
Task {
do {
let image = try await ProfileAvatarPickerLoader.loadImage(from: provider)
await MainActor.run {
let crop = ProfileAvatarCropViewController(image: image) { [weak self] data in
guard let self else { return }
Task {
await self.viewModel.uploadAvatar(
data: data,
fileName: "avatar_\(UUID().uuidString).jpg",
api: self.profileAPI
)
}
}
self.present(crop, animated: true)
}
} catch {
await MainActor.run { showToast(error.localizedDescription) }
}
}
}
}
/// Android `AddScheduleScreen`
final class ProfileAddScheduleViewController: BaseViewController {
private let selectedDate: Date
private let viewModel: ProfileSpaceSettingsViewModel
private let profileAPI: ProfileAPI
private let contentStack = UIStackView()
private let bottomBar = UIView()
private let saveButton = AppButton(title: "保存")
private let nameField = UITextField()
private let startButton = ProfileTimeButton()
private let endButton = ProfileTimeButton()
private let orderButton = UIButton(type: .system)
private let remarkTextView = UITextView()
private let remarkPlaceholderLabel = UILabel()
private let countLabel = UILabel()
private var selectedOrderNumber: String?
init(selectedDate: Date, viewModel: ProfileSpaceSettingsViewModel, profileAPI: ProfileAPI) {
self.selectedDate = selectedDate
self.viewModel = viewModel
self.profileAPI = profileAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "添加日程"
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
ProfileNavigationStyle.apply(to: navigationController, showsDivider: true)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
ProfileNavigationStyle.restore(navigationController)
}
override func setupUI() {
view.addSubview(contentStack)
view.addSubview(bottomBar)
bottomBar.addSubview(saveButton)
bottomBar.backgroundColor = .white
view.backgroundColor = AppColor.pageBackground
saveButton.isEnabled = false
saveButton.snp.updateConstraints { make in make.height.equalTo(48) }
contentStack.axis = .vertical
contentStack.spacing = 20
contentStack.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
contentStack.isLayoutMarginsRelativeArrangement = true
contentStack.backgroundColor = .white
contentStack.layer.cornerRadius = 12
contentStack.addArrangedSubview(makeTitle("日程安排"))
configureNameField()
contentStack.addArrangedSubview(nameField)
contentStack.addArrangedSubview(makeTimeRow())
configureOrderButton()
contentStack.addArrangedSubview(orderButton)
configureRemarkView()
contentStack.addArrangedSubview(remarkTextView)
contentStack.addArrangedSubview(countLabel)
}
override func setupConstraints() {
bottomBar.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
}
saveButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.leading.trailing.equalToSuperview().inset(15)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
}
contentStack.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(16)
make.leading.trailing.equalToSuperview().inset(16)
}
nameField.snp.makeConstraints { make in
make.height.equalTo(44)
}
orderButton.snp.makeConstraints { make in
make.height.equalTo(44)
}
remarkTextView.snp.makeConstraints { make in
make.height.equalTo(100)
}
}
override func bindActions() {
saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
startButton.addTarget(self, action: #selector(startTapped), for: .touchUpInside)
endButton.addTarget(self, action: #selector(endTapped), for: .touchUpInside)
orderButton.addTarget(self, action: #selector(orderTapped), for: .touchUpInside)
remarkTextView.delegate = self
nameField.addTarget(self, action: #selector(formValueChanged), for: .editingChanged)
}
private func makeTitle(_ title: String) -> UILabel {
let label = UILabel()
label.text = title
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}
private func configureNameField() {
nameField.placeholder = "请输入日程名称"
nameField.font = .systemFont(ofSize: 14)
nameField.returnKeyType = .done
nameField.delegate = self
nameField.layer.borderColor = AppColor.border.cgColor
nameField.layer.borderWidth = 1
nameField.layer.cornerRadius = 8
nameField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
nameField.leftViewMode = .always
}
private func makeTimeRow() -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 12
let title = UILabel()
title.text = "时间"
title.font = .systemFont(ofSize: 14)
title.textColor = UIColor(hex: 0x4B5563)
let toLabel = UILabel()
toLabel.text = ""
toLabel.font = .systemFont(ofSize: 14)
toLabel.textColor = UIColor(hex: 0x4B5563)
[startButton, endButton].forEach { button in
button.setTitle("请选择", for: .normal)
button.snp.makeConstraints { make in
make.width.equalTo(60)
make.height.equalTo(36)
}
}
row.addArrangedSubview(title)
row.addArrangedSubview(UIView())
row.addArrangedSubview(startButton)
row.addArrangedSubview(toLabel)
row.addArrangedSubview(endButton)
return row
}
private func configureOrderButton() {
orderButton.setTitle("关联订单 (可选)", for: .normal)
orderButton.setTitleColor(.black, for: .normal)
orderButton.contentHorizontalAlignment = .left
orderButton.titleLabel?.font = .systemFont(ofSize: 14)
orderButton.layer.borderColor = AppColor.border.cgColor
orderButton.layer.borderWidth = 1
orderButton.layer.cornerRadius = 8
orderButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
var configuration = UIButton.Configuration.plain()
configuration.title = "关联订单 (可选)"
configuration.image = UIImage(systemName: "chevron.down")
configuration.imagePlacement = .trailing
configuration.imagePadding = 8
configuration.baseForegroundColor = .black
configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
orderButton.configuration = configuration
}
private func configureRemarkView() {
remarkTextView.font = .systemFont(ofSize: 14)
remarkTextView.textColor = UIColor(hex: 0x4B5563)
remarkTextView.layer.borderColor = UIColor(hex: 0xE5E7EB).cgColor
remarkTextView.layer.borderWidth = 1
remarkTextView.layer.cornerRadius = 8
remarkTextView.textContainerInset = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12)
remarkTextView.textContainer.lineFragmentPadding = 0
remarkPlaceholderLabel.text = "请输入日程内容"
remarkPlaceholderLabel.font = remarkTextView.font
remarkPlaceholderLabel.textColor = UIColor(hex: 0xB6BECA)
remarkTextView.addSubview(remarkPlaceholderLabel)
remarkPlaceholderLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.leading.equalToSuperview().offset(12)
make.trailing.equalToSuperview().inset(12)
}
countLabel.text = "0/200"
countLabel.textAlignment = .right
countLabel.font = .systemFont(ofSize: 12)
countLabel.textColor = UIColor(hex: 0xB6BECA)
}
private func showTimePicker(initialTime: String?, onConfirm: @escaping (String) -> Void) {
let date = Self.date(from: initialTime)
let dialog = ProfileTimePickerDialogViewController(initialDate: date) { selectedDate in
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm"
onConfirm(formatter.string(from: selectedDate))
}
present(dialog, animated: false)
}
@objc private func startTapped() {
showTimePicker(initialTime: startButton.title(for: .normal)) { [weak self] value in
if let end = self?.endButton.title(for: .normal), end != "请选择", !ProfileSpaceSettingsViewModel.isStart(value, before: end) {
self?.showToast("结束时间必须晚于开始时间,已清除结束时间")
self?.endButton.setTitle("请选择", for: .normal)
}
self?.startButton.setTitle(value, for: .normal)
self?.updateSaveEnabled()
}
}
@objc private func endTapped() {
showTimePicker(initialTime: endButton.title(for: .normal)) { [weak self] value in
if let start = self?.startButton.title(for: .normal), start != "请选择", !ProfileSpaceSettingsViewModel.isStart(start, before: value) {
self?.showToast("结束时间必须晚于开始时间")
return
}
self?.endButton.setTitle(value, for: .normal)
self?.updateSaveEnabled()
}
}
@objc private func orderTapped() {
let controller = TaskOrderSelectViewController(initialOrderNumber: selectedOrderNumber)
controller.onConfirmed = { [weak self] orderNumber in
self?.selectedOrderNumber = orderNumber
self?.applySelectedOrder()
}
navigationController?.pushViewController(controller, animated: true)
}
@objc private func saveTapped() {
view.endEditing(true)
let start = startButton.title(for: .normal) == "请选择" ? nil : startButton.title(for: .normal)
let end = endButton.title(for: .normal) == "请选择" ? nil : endButton.title(for: .normal)
Task {
let success = await viewModel.addSchedule(
name: nameField.text ?? "",
remark: remarkTextView.text ?? "",
startTime: start,
endTime: end,
scheduleDate: selectedDate,
orderNumber: selectedOrderNumber,
api: profileAPI
)
if success {
await MainActor.run { navigationController?.popViewController(animated: true) }
}
}
}
@objc private func formValueChanged() {
updateSaveEnabled()
}
private func updateSaveEnabled() {
let name = (nameField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let start = startButton.title(for: .normal)
let end = endButton.title(for: .normal)
saveButton.isEnabled = !name.isEmpty
&& start != nil && start != "请选择"
&& end != nil && end != "请选择"
&& ProfileSpaceSettingsViewModel.isStart(start ?? "", before: end ?? "")
}
private func applySelectedOrder() {
var configuration = orderButton.configuration ?? .plain()
configuration.title = selectedOrderNumber ?? "关联订单 (可选)"
configuration.baseForegroundColor = .black
orderButton.configuration = configuration
}
private static func date(from time: String?) -> Date {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm"
return formatter.date(from: time ?? "")
?? formatter.date(from: "00:00")
?? Date(timeIntervalSince1970: 0)
}
}
extension ProfileAddScheduleViewController: UITextFieldDelegate, UITextViewDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textViewDidChange(_ textView: UITextView) {
if textView.text.count > 200 {
textView.text = String(textView.text.prefix(200))
}
remarkPlaceholderLabel.isHidden = !textView.text.isEmpty
countLabel.text = "\(textView.text.count)/200"
}
func textView(
_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText text: String
) -> Bool {
guard text == "\n" else { return true }
textView.resignFirstResponder()
return false
}
}
///
private final class ProfileTimeButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel?.font = .systemFont(ofSize: 14)
titleLabel?.lineBreakMode = .byClipping
setTitleColor(UIColor(hex: 0x4B5563), for: .normal)
backgroundColor = AppColor.inputBackground
layer.cornerRadius = 4
contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
contentHorizontalAlignment = .center
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
bounds.insetBy(dx: -4, dy: -4).contains(point)
}
}
///
private final class ProfileDateButton: UIButton {
var date: Date?
private let weekdayLabel = UILabel()
private let dayContainer = UIView()
private let dayLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
weekdayLabel.font = .systemFont(ofSize: 14)
weekdayLabel.textAlignment = .center
weekdayLabel.textColor = UIColor(hex: 0x4B5563)
dayContainer.layer.cornerRadius = 4
dayContainer.isUserInteractionEnabled = false
dayLabel.font = .systemFont(ofSize: 14)
dayLabel.textAlignment = .center
dayContainer.addSubview(dayLabel)
addSubview(weekdayLabel)
addSubview(dayContainer)
weekdayLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(18)
}
dayContainer.snp.makeConstraints { make in
make.top.equalTo(weekdayLabel.snp.bottom).offset(12)
make.centerX.equalToSuperview()
make.width.height.equalTo(36)
make.bottom.equalToSuperview()
}
dayLabel.snp.makeConstraints { make in make.center.equalToSuperview() }
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(weekday: String, day: String, selected: Bool) {
weekdayLabel.text = weekday
dayLabel.text = day
dayLabel.textColor = selected ? .white : UIColor(hex: 0x4B5563)
dayContainer.backgroundColor = selected ? AppColor.primary : AppColor.inputBackground
accessibilityLabel = "星期\(weekday)\(day)"
accessibilityTraits = selected ? [.button, .selected] : .button
}
}
///
private final class ProfileTagWrapView: UIView {
enum Style {
case label
case certification
case device
}
private var itemViews: [UIView] = []
private var removeHandler: ((String) -> Void)?
private weak var trailingView: UIView?
func setTrailingView(_ view: UIView) {
trailingView = view
if view.superview !== self {
addSubview(view)
}
itemViews.append(view)
invalidateIntrinsicContentSize()
setNeedsLayout()
}
func setItems(_ items: [String], style: Style, removable: Bool, onRemove: ((String) -> Void)? = nil) {
subviews.forEach { $0.removeFromSuperview() }
itemViews = items.map { item in
let view = makeItem(title: item, style: style, removable: removable)
addSubview(view)
return view
}
if let trailingView {
addSubview(trailingView)
itemViews.append(trailingView)
}
removeHandler = onRemove
invalidateIntrinsicContentSize()
setNeedsLayout()
}
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: measuredHeight(for: bounds.width > 0 ? bounds.width : UIScreen.main.bounds.width - 62))
}
override func layoutSubviews() {
super.layoutSubviews()
layoutItems(width: bounds.width)
}
private func layoutItems(width: CGFloat) {
let spacing: CGFloat = 8
var x: CGFloat = 0
var y: CGFloat = 0
var rowHeight: CGFloat = 0
for view in itemViews {
guard !view.isHidden else { continue }
let size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if x > 0, x + size.width > width {
x = 0
y += rowHeight + spacing
rowHeight = 0
}
view.frame = CGRect(x: x, y: y, width: size.width, height: size.height)
x += size.width + spacing
rowHeight = max(rowHeight, size.height)
}
}
private func measuredHeight(for width: CGFloat) -> CGFloat {
guard !itemViews.isEmpty else { return 1 }
let spacing: CGFloat = 8
var x: CGFloat = 0
var height: CGFloat = 0
var rowHeight: CGFloat = 0
for view in itemViews {
guard !view.isHidden else { continue }
let size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if x > 0, x + size.width > width {
x = 0
height += rowHeight + spacing
rowHeight = 0
}
x += size.width + spacing
rowHeight = max(rowHeight, size.height)
}
return height + rowHeight
}
private func makeItem(title: String, style: Style, removable: Bool) -> UIView {
let button = UIButton(type: .system)
button.setTitle(removable ? "\(title) ×" : title, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: style == .device ? 14 : 12)
button.contentEdgeInsets = UIEdgeInsets(top: 2, left: style == .device ? 8 : 4, bottom: 2, right: style == .device ? 8 : 4)
button.layer.cornerRadius = 4
switch style {
case .label:
button.backgroundColor = AppColor.primaryLight
button.setTitleColor(AppColor.primary, for: .normal)
case .certification:
button.backgroundColor = AppColor.warningBackground
button.setTitleColor(AppColor.warning, for: .normal)
case .device:
button.backgroundColor = AppColor.inputBackground
button.setTitleColor(UIColor(hex: 0x4B5563), for: .normal)
button.snp.makeConstraints { make in
make.height.equalTo(28)
}
}
if removable {
button.addAction(UIAction { [weak self] _ in
self?.removeHandler?(title)
}, for: .touchUpInside)
}
return button
}
}
///
private final class ProfileScheduleItemView: UIStackView {
var onDelete: (() -> Void)?
var onCall: ((String) -> Void)?
private let schedule: PhotographerSchedule
init(schedule: PhotographerSchedule) {
self.schedule = schedule
super.init(frame: .zero)
setupUI()
}
@available(*, unavailable)
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
axis = .vertical
spacing = 4
backgroundColor = UIColor(hex: 0xF9FAFB)
layer.cornerRadius = 8
layoutMargins = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
isLayoutMarginsRelativeArrangement = true
let top = UIStackView()
top.axis = .horizontal
top.spacing = 8
let title = UILabel()
title.text = "\(schedule.startTime) - \(schedule.endTime) \(schedule.name)"
title.font = .systemFont(ofSize: 14)
title.textColor = UIColor(hex: 0x4B5563)
title.numberOfLines = 1
let delete = UIButton(type: .system)
delete.setTitle("删除", for: .normal)
delete.setTitleColor(UIColor(hex: 0xFF5656), for: .normal)
delete.titleLabel?.font = .systemFont(ofSize: 14)
delete.addAction(UIAction { [weak self] _ in self?.onDelete?() }, for: .touchUpInside)
top.addArrangedSubview(title)
top.addArrangedSubview(delete)
addArrangedSubview(top)
if let order = schedule.orderNumber, !order.isEmpty {
addArrangedSubview(makeLine("订单号: \(order)"))
}
if let phone = schedule.userPhone, !phone.isEmpty {
let row = UIStackView()
row.axis = .horizontal
row.spacing = 16
row.addArrangedSubview(makeLine("客户手机号: \(Self.maskPhone(phone))"))
let call = UIButton(type: .system)
call.setTitle("拨打", for: .normal)
call.setTitleColor(AppColor.primary, for: .normal)
call.titleLabel?.font = .systemFont(ofSize: 14)
call.addAction(UIAction { [weak self] _ in self?.onCall?(phone) }, for: .touchUpInside)
row.addArrangedSubview(call)
addArrangedSubview(row)
}
if !schedule.remark.isEmpty {
addArrangedSubview(makeLine("备注信息: \(schedule.remark)"))
}
}
private func makeLine(_ text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = .systemFont(ofSize: 14)
label.textColor = UIColor(hex: 0x4B5563)
label.numberOfLines = 1
return label
}
private static func maskPhone(_ phone: String) -> String {
guard phone.count >= 11 else { return phone }
let prefix = phone.prefix(3)
let suffix = phone.suffix(phone.count - 7)
return "\(prefix)****\(suffix)"
}
}
///
private enum ProfileAvatarPickerLoader {
static func loadImage(from provider: NSItemProvider) async throws -> UIImage {
try await withCheckedThrowingContinuation { continuation in
guard provider.canLoadObject(ofClass: UIImage.self) else {
continuation.resume(throwing: OSSUploadError.unsupportedFileType)
return
}
provider.loadObject(ofClass: UIImage.self) { object, error in
if let error {
continuation.resume(throwing: error)
return
}
guard let image = object as? UIImage else {
continuation.resume(throwing: OSSUploadError.emptyFile)
return
}
continuation.resume(returning: image)
}
}
}
}