I’m currently developing some update in our views and we have a view which is totally made in SwiftUI. This view has ScrollView
, which before we nested a UIViewRepresentable
, it used to scroll to the TextField
, when this field was focused.
struct PinDetailViewSwiftUI {
var body: some View {
ScrollView {
VStack(spacing: 0) {
GeometryReader { proxy in
let offset = proxy.frame(in: .named("scroll")).minY
Color(GrayColors.gray10.getColor()).preference(key: ViewOffsetKey.self, value: offset)
}
if pinViewModel.isLoadingData {
PinDetailLoadingViewSwiftUI()
} else {
PinSectionHeaderView(title: DTTaskListTranslate.splitViewTasks.translation)
if !pinViewModel.tasks.isEmpty || pinViewModel.tasksHasChanged {
NavigationLink(destination: PinTaskDetailSwiftUI(tasks: .constant(pinViewModel.tasks), selectedTask: pinViewModel.preSelectedTask, pin: pinViewModel.pinModel, userRole: project.roleType ?? .SubContractor, viewMode: .view, reassign: false), isActive: $pinViewModel.openPreSelectedTask) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
SwiftUI.Group {
TaskPreviewRepresentable(task: pinViewModel.tasks[0], userImage: .constant(nil), userRole: project.roleType ?? .SubContractor, viewModel: viewModel)
.frame(height: viewModel.getHeightOfTaskPreview(task: task, userRole: project.roleType ?? .SubContractor), alignment: .center)
}
.animation(.easeInOut)
}
if pinViewModel.pinModel != nil && pinViewModel.userCanCreateTasks() {
NavigationLink(isActive: $pinViewModel.showTaskCreationView) {
PinTaskDetailSwiftUI(tasks: $pinViewModel.tasks, selectedTask: DTMediaPost.getDefaultTask(pin: pinViewModel.pinModel), pin: pinViewModel.pinModel, userRole: project.roleType ?? .SubContractor, viewMode: .create, reassign: false)
} label: {
Button(action: {
if !pin.isPinClosedOrInactive {
pinViewModel.showTaskCreationView.toggle()
} else {
pinViewModel.showPinClosedAlert.toggle()
}
}, label: {
PinCreateNewTask()
})
}
.frame(height: 50)
}
PinSectionHeaderView(title: DTPinTranslate.pinFieldsHeader.translation)
if !pinViewModel.datasets.isEmpty {
PinDatasetView(linkedDatasets: $pinViewModel.linkedRecords, records: $pinViewModel.datasets, datasetChanged: $pinViewModel.datasetChanged, showPinClosedAlert: $pinViewModel.showPinClosedAlert, pin: pinViewModel.pinModel)
Rectangle()
.frame(maxWidth: .infinity)
.frame(height: 1)
.foregroundColor(.gray.opacity(0.2))
.padding([.bottom, .top], 8)
}
if !pinViewModel.pinFields.isEmpty {
SwiftUI.Group {
ForEach(pinViewModel.pinFields, id: .id!) { value in
switch value.type! {
case .number:
PinTextFieldView(pinModel: $pinViewModel.pinModel, numberPinField: value as! DTNumberPinField, canEdit: pinViewModel.pinModel.canPinBeChanged)
case .date:
PinDateView(pin: $pinViewModel.pinModel, dtPinDateField: value as! DTPinDateField, canEdit: pinViewModel.pinModel.canPinBeChanged)
case .slider:
PinSliderView(pinSliderScale: value as! DTPinSliderField, pinModel: $pinViewModel.pinModel, categoryColor: $pinViewModel.pinCategoryColor, canEdit: pinViewModel.pinModel.canPinBeChanged)
case .text:
PinTextAreaView(textPinField: value as! DTTextPinField, canEdit: pinViewModel.pinModel.canPinBeChanged, pinModel: $pinViewModel.pinModel)
case .tags:
VStack {
if showTagSuggestions {
PinTagSuggestionView(suggestedTags: $pinViewModel.suggestedTags, recentTags: $pinViewModel.filteredRecentTags, searchedTag: $pinViewModel.currentTagBeingAdded)
.frame(height: pinViewModel.getRecentTagsHeight())
}
PinTagsView(showTagSuggestions: $showTagSuggestions, currentTagBeingAdded: $pinViewModel.currentTagBeingAdded, canBeEdited: pinViewModel.pinModel.canPinBeChanged, tagField: value, pin: $pinViewModel.pinModel)
}
case .time:
PinTimeFieldView(pinModel: $pinViewModel.pinModel, timePinField: value as! DTPinTimeField)
}
Rectangle()
.frame(maxWidth: .infinity)
.frame(height: 1)
.foregroundColor(.gray.opacity(0.2))
.padding([.bottom, .top], 8)
}
}
}
if !pinViewModel.visualMediaPosts.isEmpty, !isCameraOpened {
PinMediaView(medias: $pinViewModel.visualMediaPosts, pin: pinViewModel.pinModel, numberOfMedias: pinViewModel.numberOfElementsInPin())
.frame(height: pinViewModel.getTotalHeightForMediaSection())
}
if !pinViewModel.mediaPosts.isEmpty,
!isCameraOpened {
PinSectionHeaderView(title: DTSharedTranslate.activity.translation)
if #available(iOS 16.0, *) {
PinActivityViewList(mediaPosts: pinViewModel.mediaPosts, pin: pinViewModel.pinModel, isLastPage: pinViewModel.isLastPage, pinDetailViewModel: .constant(pinViewModel), isLoading: pinViewModel.isLoadingMedia, loadNextPage: $pinViewModel.loadNextPage)
.frame(height: pinViewModel.getTotalHeightForActivitySection())
.animation(.easeInOut)
.scrollContentBackground(.hidden)
} else {
PinActivityViewList(mediaPosts: pinViewModel.mediaPosts, pin: pinViewModel.pinModel, isLastPage: pinViewModel.isLastPage, pinDetailViewModel: .constant(pinViewModel), isLoading: pinViewModel.isLoadingMedia, loadNextPage: $pinViewModel.loadNextPage)
.frame(height: pinViewModel.getTotalHeightForActivitySection())
.animation(.easeInOut)
}
}
}
}
.background(Color.white)
.navigationBarHidden(true)
}
.background(Color(GrayColors.gray10.getColor()))
.coordinateSpace(name: "scroll")
.onPreferenceChange(ViewOffsetKey.self) { value in
if pinViewModel.galleryViewHeight >= 0.0 && pinViewModel.galleryViewHeight <= pinViewModel.kGalleryHeightConst {
let previousValue = pinViewModel.galleryViewHeight + value
if previousValue <= 0.0 {
pinViewModel.galleryViewHeight = 0
} else if previousValue >= pinViewModel.kGalleryHeightConst {
pinViewModel.galleryViewHeight = pinViewModel.kGalleryHeightConst
} else {
pinViewModel.galleryViewHeight = previousValue
}
}
}
}
}
As you can see in the code above, the: PinTextFieldView
, PinDateView
, PinTextAreaView
are all text fields, which when we tap on it, moves the scroll view and focus them.
This behaviour used to work fine, until the moment we added a new class: TaskPreviewRepresentable
, which is just a XIB done in UIKit, which renders another object that we fetch from the backend.
In this view we have only a few buttons and labels, all the constraints are set up correctly, but for some reason, I still can’t get the ScrollView, to scroll to the field when it’s focused.
Has anyone ever faced this issue, which the ScrollView stops to focus on a text field, only because you added a UIViewRepresentable to it?
This is the code for the TaskPreviewRepresentable
struct TaskPreviewRepresentable: View, UIViewRepresentable {
@Binding var task: DTMediaPost
@Binding var userImage: UIImage?
var userRole: RoleType
@SwiftUI.State var viewModel: CardTaskViewModel
func makeUIView(context: Context) -> DTTaskPreview {
let taskPreview = DTTaskPreview(frame: .init(x: 0, y: 0, width: UIDevice.isiPad ? viewModel.pinDetailiPadWidth - 32 : UIScreen.main.bounds.width - 32, height: self.viewModel.getHeightOfTaskPreview(task: task, userRole: userRole)))
taskPreview.setupTaskStatus(taskStatus: NewTaskStatus(dtMediaPost: task))
taskPreview.setTaskStatusLabel(value: NewTaskStatus(dtMediaPost: task).getName().lowercased())
taskPreview.setTaskNumber(value: DTTaskListTranslate.taskListNumber.translation+" "+(task.number.unwrapped == 0 ? "-" : String(task.number.unwrapped)))
taskPreview.setTaskDescription(value: task.title.unwrapped)
taskPreview.setTaskCreatedOrLastModified(value: viewModel.getLastModifiedString(task: task))
taskPreview.setAssigneeImage(image: viewModel.userImage, placeholder: task.getAssigneePlaceholder)
var mainAction: TaskQuickAction?
var secondaryAction: TaskQuickAction?
(secondaryAction, mainAction) = viewModel.getMainAndSecondaryAction(userRole: userRole, task: task)
if let leftAction = secondaryAction {
taskPreview.setLeftButton(buttonSize: .medium, buttonStyle: .tertiary)
taskPreview.setActionForLeftButton(actionName: leftAction.getOptionTranslation(), quickAction: leftAction)
} else {
taskPreview.hideLeftButton(hidden: true)
}
if let mainAction = mainAction {
taskPreview.setRightButton(buttonSize: .medium, buttonStyle: .callToAction)
taskPreview.setActionForRightButton(actionName: mainAction.getOptionTranslation(), quickAction: mainAction)
} else {
taskPreview.hideRightButton(hidden: true)
}
taskPreview.hideButtonView(hidden: (mainAction == nil && secondaryAction == nil))
taskPreview.setAssigneeName(value: task.assignment?.name ?? "")
taskPreview.setAssigneeSecondLine(value: viewModel.getAssigneeSecondLineString(task: task))
taskPreview.setDueDateLabel(value: task.due)
taskPreview.delegate = viewModel
return taskPreview
}
func updateUIView(_ uiView: DTTaskPreview, context: Context) {
uiView.setAssigneeImage(image: viewModel.userImage, placeholder: task.getAssigneePlaceholder)
}
}