스위프트 만드는 법UI 목록이 자동으로 스크롤됩니까?
목록 보기에 내용을 추가할 때 자동으로 아래로 스크롤합니다.
Swift UI를 사용하고 있습니다.List
a 리고a.BindableObject
관리자로 지정합니다.새 데이터를 목록에 추가하는 중입니다.
List(chatController.messages, id: \.self) { message in
MessageView(message.text, message.isMe)
}
메시지 목록에 새 데이터를 추가할 때 목록이 아래로 스크롤되기를 원합니다.하지만 수동으로 아래로 스크롤해야 합니다.
업데이트: iOS 14에서는 이제 기본적인 방법으로 이를 수행이 가능합니다.그렇게 하고 있습니다.
ScrollViewReader { scrollView in
ScrollView(.vertical) {
LazyVStack {
ForEach(notes, id: \.self) { note in
MessageView(note: note)
}
}
.onAppear {
scrollView.scrollTo(notes[notes.endIndex - 1])
}
}
}
iOS 13 이하에서는 다음을 시도할 수 있습니다.
저는 보기를 뒤집는 것이 저에게 꽤 잘 맞는 것 같다는 것을 발견했습니다.이렇게 하면 맨 아래에서 ScrollView가 시작되고 새 데이터를 추가하면 보기가 자동으로 아래로 스크롤됩니다.
- 회전 180도 회전
.rotationEffect(.radians(.pi))
- .
.scaleEffect(x: -1, y: 1, anchor: .center)
이제 모든 뷰가 회전 및 플립되므로 내부 뷰에도 이 작업을 수행해야 합니다.위와 같은 방법으로 뒤집습니다.
이렇게 많은 장소가 필요하다면 이를 위한 사용자 정의 보기를 사용할 가치가 있습니다.
다음과 같은 방법을 사용할 수 있습니다.
List(chatController.messages, id: \.self) { message in
MessageView(message.text, message.isMe)
.rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
}
.rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
뒤집을 수 있는 보기 확장자가 있습니다.
extension View {
public func flip() -> some View {
return self
.rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
}
}
현재 그러한 기능이 내장되어 있지 않기 때문에(리스트용도 ScrollView용도 아님) Xcode 11.2이므로 ScrollToEnd 동작으로 사용자 정의 ScrollView를 코딩해야 했습니다.
이 기사에서 영감을 받았습니다.
여기 제가 실험한 결과가 있습니다. 도움이 되길 바랍니다.물론 색상 등과 같이 구성 가능한 매개 변수가 더 많지만, 사소한 것처럼 보이고 범위를 벗어납니다.
import SwiftUI
struct ContentView: View {
@State private var objects = ["0", "1"]
var body: some View {
NavigationView {
VStack {
CustomScrollView(scrollToEnd: true) {
ForEach(self.objects, id: \.self) { object in
VStack {
Text("Row \(object)").padding().background(Color.yellow)
NavigationLink(destination: Text("Details for \(object)")) {
Text("Link")
}
Divider()
}.overlay(RoundedRectangle(cornerRadius: 8).stroke())
}
}
.navigationBarTitle("ScrollToEnd", displayMode: .inline)
// CustomScrollView(reversed: true) {
// ForEach(self.objects, id: \.self) { object in
// VStack {
// Text("Row \(object)").padding().background(Color.yellow)
// NavigationLink(destination: Text("Details for \(object)")) {
// Image(systemName: "chevron.right.circle")
// }
// Divider()
// }.overlay(RoundedRectangle(cornerRadius: 8).stroke())
// }
// }
// .navigationBarTitle("Reverse", displayMode: .inline)
HStack {
Button(action: {
self.objects.append("\(self.objects.count)")
}) {
Text("Add")
}
Button(action: {
if !self.objects.isEmpty {
self.objects.removeLast()
}
}) {
Text("Remove")
}
}
}
}
}
}
struct CustomScrollView<Content>: View where Content: View {
var axes: Axis.Set = .vertical
var reversed: Bool = false
var scrollToEnd: Bool = false
var content: () -> Content
@State private var contentHeight: CGFloat = .zero
@State private var contentOffset: CGFloat = .zero
@State private var scrollOffset: CGFloat = .zero
var body: some View {
GeometryReader { geometry in
if self.axes == .vertical {
self.vertical(geometry: geometry)
} else {
// implement same for horizontal orientation
}
}
.clipped()
}
private func vertical(geometry: GeometryProxy) -> some View {
VStack {
content()
}
.modifier(ViewHeightKey())
.onPreferenceChange(ViewHeightKey.self) {
self.updateHeight(with: $0, outerHeight: geometry.size.height)
}
.frame(height: geometry.size.height, alignment: (reversed ? .bottom : .top))
.offset(y: contentOffset + scrollOffset)
.animation(.easeInOut)
.background(Color.white)
.gesture(DragGesture()
.onChanged { self.onDragChanged($0) }
.onEnded { self.onDragEnded($0, outerHeight: geometry.size.height) }
)
}
private func onDragChanged(_ value: DragGesture.Value) {
self.scrollOffset = value.location.y - value.startLocation.y
}
private func onDragEnded(_ value: DragGesture.Value, outerHeight: CGFloat) {
let scrollOffset = value.predictedEndLocation.y - value.startLocation.y
self.updateOffset(with: scrollOffset, outerHeight: outerHeight)
self.scrollOffset = 0
}
private func updateHeight(with height: CGFloat, outerHeight: CGFloat) {
let delta = self.contentHeight - height
self.contentHeight = height
if scrollToEnd {
self.contentOffset = self.reversed ? height - outerHeight - delta : outerHeight - height
}
if abs(self.contentOffset) > .zero {
self.updateOffset(with: delta, outerHeight: outerHeight)
}
}
private func updateOffset(with delta: CGFloat, outerHeight: CGFloat) {
let topLimit = self.contentHeight - outerHeight
if topLimit < .zero {
self.contentOffset = .zero
} else {
var proposedOffset = self.contentOffset + delta
if (self.reversed ? proposedOffset : -proposedOffset) < .zero {
proposedOffset = 0
} else if (self.reversed ? proposedOffset : -proposedOffset) > topLimit {
proposedOffset = (self.reversed ? topLimit : -topLimit)
}
self.contentOffset = proposedOffset
}
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
extension ViewHeightKey: ViewModifier {
func body(content: Content) -> some View {
return content.background(GeometryReader { proxy in
Color.clear.preference(key: Self.self, value: proxy.size.height)
})
}
}
iOS 14/15:
저는 그것을 사용하여 했습니다.onChange
ScrollView의 수식어는 다음과 같습니다.
// View
struct ChatView: View {
@ObservedObject var viewModel = ChatViewModel()
@State var newText = ""
var body: some View {
ScrollViewReader { scrollView in
VStack {
ScrollView(.vertical) {
VStack {
ForEach(viewModel.messages) { message in
VStack {
Text(message.text)
Divider()
}
}
}.id("ChatScrollView")
}.onChange(of: viewModel.messages) { _ in
withAnimation {
scrollView.scrollTo("ChatScrollView", anchor: .bottom)
}
}
Spacer()
VStack {
TextField("Enter message", text: $newText)
.padding()
.frame(width: 400, height: 40, alignment: .center)
Button("Send") {
viewModel.addMessage(with: newText)
}
.frame(width: 400, height: 80, alignment: .center)
}
}
}
}
}
// View Model
class ChatViewModel: ObservableObject {
@Published var messages: [Message] = [Message]()
func addMessage(with text: String) {
messages.append(Message(text: text))
}
}
// Message Model
struct Message: Hashable, Codable, Identifiable {
var id: String = UUID().uuidString
var text: String
}
Swift UI 2.0 - iOS 14
과 같습니다. (으로합니다.)ScrollViewReader
)
scrollView.scrollTo(rowID)
Swift 위프출시함와 UI 의 UI 2.0에 한 모든 파일을 할 수 .ScrollViewReader
그런 다음 스크롤해야 하는 정확한 요소 위치에 액세스할 수 있습니다.
다음은 전체 데모 앱입니다.
// A simple list of messages
struct MessageListView: View {
var messages = (1...100).map { "Message number: \($0)" }
var body: some View {
ScrollView {
LazyVStack {
ForEach(messages, id:\.self) { message in
Text(message)
Divider()
}
}
}
}
}
struct ContentView: View {
@State var search: String = ""
var body: some View {
ScrollViewReader { scrollView in
VStack {
MessageListView()
Divider()
HStack {
TextField("Number to search", text: $search)
Button("Go") {
withAnimation {
scrollView.scrollTo("Message number: \(search)")
}
}
}.padding(.horizontal, 16)
}
}
}
}
미리보기
iOS 13+
ScrollViewProxy라는 이 패키지는 호출할 수 있는 ScrollViewProxy를 제공하는 ScrollViewReader를 추가합니다.scrollTo(_:)
사용자가 View에 제공한 모든 ID에 대해.후드 아래에서는 Introspect를 사용하여 UIScrollView를 가져옵니다.
예:
ScrollView {
ScrollViewReader { proxy in
Button("Jump to #8") {
proxy.scrollTo(8)
}
ForEach(0..<10) { i in
Text("Example \(i)")
.frame(width: 300, height: 300)
.scrollId(i)
}
}
}
Xcode 12부터 all new를 사용하면 다음과 같은 예제 코드를 사용할 수 있습니다.
은 당신의 아래코업수있다니로 아래 할 수 .chatController.messages
그리고 결정.scrollViewProxy.scrollTo(chatController.messages.count-1)
.
언제 할까요?아마 스위프트에서UI의 새 기능onChange
!
struct ContentView: View {
let itemCount: Int = 100
var body: some View {
ScrollViewReader { scrollViewProxy in
VStack {
Button("Scroll to top") {
scrollViewProxy.scrollTo(0)
}
Button("Scroll to buttom") {
scrollViewProxy.scrollTo(itemCount-1)
}
ScrollView {
LazyVStack {
ForEach(0 ..< itemCount) { i in
Text("Item \(i)")
.frame(height: 50)
.id(i)
}
}
}
}
}
}
}
이것은 NSViewControllerRepresentable 객체 안에 NSCrollView를 감아서 macOS에서 수행할 수 있습니다(그리고 저는 UIScrollView와 UIViewControllerRepresentable을 사용하여 iOS에서도 동일하게 작동한다고 가정합니다).OS가 여전히 컨트롤의 많은 기능을 관리할 것이기 때문에 저는 이것이 여기 있는 다른 답변보다 조금 더 신뢰할 수 있다고 생각합니다.
방금 이 작업을 시작했으며, 컨텐츠 내에서 특정 행의 위치를 지정하는 등 몇 가지 작업을 더 시도할 계획입니다. 하지만 지금까지 사용한 코드는 다음과 같습니다.
import SwiftUI
struct ScrollableView<Content:View>: NSViewControllerRepresentable {
typealias NSViewControllerType = NSScrollViewController<Content>
var scrollPosition : Binding<CGPoint?>
var hasScrollbars : Bool
var content: () -> Content
init(hasScrollbars: Bool = true, scrollTo: Binding<CGPoint?>, @ViewBuilder content: @escaping () -> Content) {
self.scrollPosition = scrollTo
self.hasScrollbars = hasScrollbars
self.content = content
}
func makeNSViewController(context: NSViewControllerRepresentableContext<Self>) -> NSViewControllerType {
let scrollViewController = NSScrollViewController(rootView: self.content())
scrollViewController.scrollView.hasVerticalScroller = hasScrollbars
scrollViewController.scrollView.hasHorizontalScroller = hasScrollbars
return scrollViewController
}
func updateNSViewController(_ viewController: NSViewControllerType, context: NSViewControllerRepresentableContext<Self>) {
viewController.hostingController.rootView = self.content()
if let scrollPosition = self.scrollPosition.wrappedValue {
viewController.scrollView.contentView.scroll(scrollPosition)
DispatchQueue.main.async(execute: {self.scrollPosition.wrappedValue = nil})
}
viewController.hostingController.view.frame.size = viewController.hostingController.view.intrinsicContentSize
}
}
class NSScrollViewController<Content: View> : NSViewController, ObservableObject {
var scrollView = NSScrollView()
var scrollPosition : Binding<CGPoint>? = nil
var hostingController : NSHostingController<Content>! = nil
@Published var scrollTo : CGFloat? = nil
override func loadView() {
scrollView.documentView = hostingController.view
view = scrollView
}
init(rootView: Content) {
self.hostingController = NSHostingController<Content>(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
struct ScrollableViewTest: View {
@State var scrollTo : CGPoint? = nil
var body: some View {
ScrollableView(scrollTo: $scrollTo)
{
Text("Scroll to bottom").onTapGesture {
self.$scrollTo.wrappedValue = CGPoint(x: 0,y: 1000)
}
ForEach(1...50, id: \.self) { (i : Int) in
Text("Test \(i)")
}
Text("Scroll to top").onTapGesture {
self.$scrollTo.wrappedValue = CGPoint(x: 0,y: 0)
}
}
}
}
Apple이 사용 가능한 방법을 개선할 때까지 Introspect 라이브러리를 사용하여 UITableView 참조를 얻는 다른 솔루션을 제시합니다.
struct LandmarkList: View {
@EnvironmentObject private var userData: UserData
@State private var tableView: UITableView?
private var disposables = Set<AnyCancellable>()
var body: some View {
NavigationView {
VStack {
List(userData.landmarks, id: \.id) { landmark in
LandmarkRow(landmark: landmark)
}
.introspectTableView { (tableView) in
if self.tableView == nil {
self.tableView = tableView
print(tableView)
}
}
}
.navigationBarTitle(Text("Landmarks"))
.onReceive(userData.$landmarks) { (id) in
// Do something with the table for example scroll to the bottom
self.tableView?.setContentOffset(CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude), animated: false)
}
}
}
}
여기 대화를 통해 채워지는 채팅의 메시지 배열과 같이 동적으로 데이터를 얻는 관찰된 개체에 대한 제 작업 솔루션이 있습니다.
메시지 배열 모델:
struct Message: Identifiable, Codable, Hashable {
//MARK: Attributes
var id: String
var message: String
init(id: String, message: String){
self.id = id
self.message = message
}
}
실제 보기:
@ObservedObject var messages = [Message]()
@State private var scrollTarget: Int?
var scrollView : some View {
ScrollView(.vertical) {
ScrollViewReader { scrollView in
ForEach(self.messages) { msg in
Text(msg).id(message.id)
}
//When you add new element it will scroll automatically to last element or its ID
.onChange(of: scrollTarget) { target in
withAnimation {
scrollView.scrollTo(target, anchor: .bottom)
}
}
.onReceive(self.$messages) { updatedMessages in
//When new element is added to observed object/array messages, change the scroll position to bottom, or last item in observed array
scrollView.scrollTo(umessages.id, anchor: .bottom)
//Update the scrollTarget to current position
self.scrollTarget = updatedChats.first!.messages.last!.message_timestamp
}
}
}
}
GitHub: https://github.com/kenagt/AutoScrollViewExample 에서 완전히 작동하는 예제를 살펴 보십시오.
◦간소화할 수 있습니다...
.onChange(of: messages) { target in
withAnimation {
scrollView.scrollTo(target.last?.id, anchor: .bottom)
}
}
많은 분들이 지적하신 대로.ScrollViewReader를 사용하여 메시지의 마지막 ID로 스크롤할 수 있습니다.그러나 ScrollView가 아래로 완전히 스크롤되지 않았습니다.다른 버전은 모든 메시지 아래에 불투명도 없이 정의된 ID를 텍스트에 넣는 것입니다.
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach(chatVM.chatMessagesArray, id: \.self) { chat in
MessageBubble(messageModel: chat)
}
}
Text("Hello").font(.caption).opacity(0).id(idString) // <= here!!
}
.onChange(of: chatVM.chatMessagesArray) { _ in
withAnimation {
scrollView.scrollTo(idString, anchor: .bottom)
}
}
.onAppear {
withAnimation {
scrollView.scrollTo(idString, anchor: .bottom)
}
}
}
이제 목록에서 자동 스크롤이 지원됩니다.
ScrollViewReader { proxy in
VStack {
Button("Jump to last") {
proxy.scrollTo(viewModel.messageViewModels.last?.id)
}
List(viewModel.messageViewModels) {
MessageView(viewModel: $0).id($0.id)
}
}
}
언급URL : https://stackoverflow.com/questions/57258846/how-to-make-a-swiftui-list-scroll-automatically
'source' 카테고리의 다른 글
PHP Curl 라이브러리를 사용하여 HTTP를 지속/유지합니까? (0) | 2023.09.04 |
---|---|
"동적 유형 텍스트 스타일을 사용하여 글꼴 요구를 자동으로 조정" 경고의 의미는 무엇입니까? (0) | 2023.09.04 |
가운데 텍스트로 원을 그리는 방법은? (0) | 2023.09.04 |
[NgStyle] 조건과 결합(그렇지 않은 경우) (0) | 2023.09.04 |
단순 SELECT 쿼리가 큰 테이블에서 느림 (0) | 2023.08.30 |