Skip to content

Commit

Permalink
Massive memory usage improvements, fixed formatting issues and improv…
Browse files Browse the repository at this point in the history
…ed default grouping controller
  • Loading branch information
ImTheSquid committed Feb 10, 2022
1 parent ff77d2c commit c61323c
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 125 deletions.
2 changes: 1 addition & 1 deletion Example/AllujaMessagesExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ struct ContentView: View {
messageBar = ""
}
})
.groupingOptions(groupingOptions)
.groupingOptions(groupingOptions)
.messageTimestampFormatter(messageFormatter)
.messageContextMenu { message in
Text("\(message.messageID)")
Expand Down
2 changes: 0 additions & 2 deletions Sources/AllujaMessages/Models/Containers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,4 @@ internal struct MessageContainer<MessageT: MessageType>: Identifiable, Equatable
func groupFlagsEmptyOrContains(_ flag: MessageGroupFlag) -> Bool {
groupFlags.isEmpty || groupFlags.contains(flag)
}

var size: CGSize = .zero
}
1 change: 1 addition & 0 deletions Sources/AllujaMessages/Models/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal extension View {
func messageCornerRadius(_ cornerRadius: CGFloat) -> some View {
environment(\.messageCornerRadius, cornerRadius)
}

func messageWidth(_ width: CGFloat) -> some View {
environment(\.messageWidth, width)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/AllujaMessages/Views/Items/ImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,6 @@ internal struct ImageView<MessageT: MessageType>: View {

private struct ImageView_Previews: PreviewProvider {
static var previews: some View {
ImageView(forMessage: MessagePreview(), withContext: MessagesViewContext<MessagePreview>(messages: []))
ImageView(forMessage: MessagePreview(), withContext: MessagesViewContext<MessagePreview>())
}
}
8 changes: 3 additions & 5 deletions Sources/AllujaMessages/Views/Messages/MessageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SwiftUI
internal struct MessageView<MessageT: MessageType>: View {
@Environment(\.messageWidth) private var width
@Environment(\.messageCornerRadius) private var cornerRadius
@Binding var container: MessageContainer<MessageT>
let container: MessageContainer<MessageT>
let context: MessagesViewContext<MessageT>
let timestampOffset: CGFloat

Expand Down Expand Up @@ -67,12 +67,11 @@ internal struct MessageView<MessageT: MessageType>: View {
ZStack {
if container.timestampFlag != .hidden {
HStack {
ChildSizeReader(size: $container.size) {
VStack {
if container.timestampFlag == .bottom {
Spacer()
}

Text(context.messageTimestampFormatter.string(from: message.timestamp))
.foregroundColor(.secondary)
.font(.footnote)
Expand All @@ -85,7 +84,6 @@ internal struct MessageView<MessageT: MessageType>: View {
Spacer()
}
}
}
Spacer()
}
}
Expand Down Expand Up @@ -168,6 +166,6 @@ internal struct MessageView<MessageT: MessageType>: View {

private struct MessageView_Previews: PreviewProvider {
static var previews: some View {
MessageView<MessagePreview>(container: .constant(MessageContainer<MessagePreview>(message: MessagePreview())), context: MessagesViewContext<MessagePreview>(messages: []), timestampOffset: 0)
MessageView<MessagePreview>(container: MessageContainer<MessagePreview>(message: MessagePreview()), context: MessagesViewContext<MessagePreview>(), timestampOffset: 0)
}
}
119 changes: 110 additions & 9 deletions Sources/AllujaMessages/Views/MessagesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,136 @@ public struct MessagesView<MessageT: MessageType, InputBarT: View>: View {

@FocusState private var focusInput: Bool
@State private var dragOffset: CGFloat = .zero
@State private var maxOffset: CGSize = .zero

// Holds all message data
@ObservedObject internal var context: MessagesViewContext<MessageT>
@ObservedObject internal var context = MessagesViewContext<MessageT>()

let messages: [MessageT]

var containerizedMessages: [MessageContainer<MessageT>] {
// Iterate over each message and see if the next one is last
var completeContainers: [MessageContainer<MessageT>] = []
// Whether or not to attempt to place a footer on the next iteration
var footerFallthrough: Bool = false
for (i, message) in messages.sorted(by: { $0.timestamp < $1.timestamp }).enumerated() {
// Figure out what options are needed for each message
var flags: Set<MessageGroupFlag> = []
var timestampFlag: MessageGroupTimestampFlag = .hidden

// If this is the first message OR the last message ends the group then add flag
if completeContainers.isEmpty || completeContainers[completeContainers.index(before: i)].groupFlags.contains(.endGroup) {
if case .collapseTimestamps(let anchor) = context.groupingOptions.first(where: { item in
if case .collapseTimestamps(_) = item {
return true
}
return false
}), anchor == .top {
timestampFlag = .top
}

flags.insert(.startGroup)
}

if context.messageEndsGroup(message) {
if case .collapseTimestamps(let anchor) = context.groupingOptions.first(where: { item in
if case .collapseTimestamps(_) = item {
return true
}
return false
}), anchor == .bottom {
timestampFlag = .bottom
}

flags.insert(.endGroup)
}

// If there aren't any timestamp grouping options, then display timestamp normally
if case .collapseTimestamps(_) = context.groupingOptions.first(where: { item in
if case .collapseTimestamps(_) = item {
return true
}
return false
}) {

} else {
timestampFlag = .normal
}

switch message.kind {
case .system(_):
if flags.contains(.endGroup) && !flags.contains(.startGroup) && i != messages.startIndex {
completeContainers[completeContainers.index(before: completeContainers.endIndex)].groupFlags.insert(.renderFooter)
} else if flags.contains(.startGroup) && !flags.contains(.endGroup) {
footerFallthrough = true
}
default:
if context.groupingOptions.contains(.collapseEnclosingViews) {
if flags.contains(.startGroup) {
flags.insert(.renderHeader)
}

if flags.contains(.endGroup) || footerFallthrough {
flags.insert(.renderFooter)
footerFallthrough = false
}
} else {
flags.insert(.renderHeader)
flags.insert(.renderFooter)
}

if context.groupingOptions.contains(.collapseProfilePicture) {
if flags.contains(.endGroup) {
flags.insert(.renderProfile)
} else {
flags.insert(.renderClearProfile)
}
} else {
flags.insert(.renderProfile)
}
}
completeContainers.append(.init(message: message, groupFlags: flags, timestampFlag: timestampFlag))
}

return completeContainers
}

public init(withMessages messages: [MessageT], @ViewBuilder withInputBar inputBar: @escaping () -> InputBarT) {
self.inputBar = inputBar
self.messages = messages

let context = MessagesViewContext<MessageT>(messages: messages)
context.messageEndsGroup = { message in
let index = messages.firstIndex(of: message)!
if index == messages.count - 1 {
return true
}

// Split if last message was sent more than 5 minutes ago or the sender changes
return message.timestamp.addingTimeInterval(5 * 60) < messages[index + 1].timestamp
return message.timestamp.addingTimeInterval(5 * 60) < messages[index + 1].timestamp || message.alignment != messages[index + 1].alignment
}

self.context = context
}

public var body: some View {
VStack(spacing: 0) {
// Somewhat hacky way to get the max text width from the timestamp formatter without taking up a ton of memory
ChildSizeReader(size: $maxOffset) {
ZStack {
ForEach(containerizedMessages, id: \.id) { message in
Text(context.messageTimestampFormatter.string(from: message.message.timestamp))
}
}
}
.opacity(0)
.frame(height: 0)

GeometryReader { geometry in
ScrollViewReader { value in
List {
Group {
ForEach($context.messages, id: \.id) { $message in
MessageView(container: $message, context: context, timestampOffset: geometry.size.width)
ForEach(containerizedMessages, id: \.id) { message in
MessageView(container: message, context: context, timestampOffset: geometry.size.width)
.padding([.top, .bottom], 2)
.contentShape(Rectangle())
.if(context.messageContextMenu != nil) {
Expand All @@ -58,8 +159,8 @@ public struct MessagesView<MessageT: MessageType, InputBarT: View>: View {
.onAppear {
context.proxyOnAppear?(value)
}
.onChange(of: context.messages) { messages in
context.proxyOnMessagesChange?(value, messages.map{ $0.message })
.onChange(of: messages) { messages in
context.proxyOnMessagesChange?(value, messages)
}
.listRowInsets(EdgeInsets())
.listRowSeparator(.hidden)
Expand All @@ -69,7 +170,7 @@ public struct MessagesView<MessageT: MessageType, InputBarT: View>: View {
$0.gesture(
DragGesture(minimumDistance: 25.0)
.onChanged { value in
dragOffset = max(min(value.translation.width, 0), -context.maxTimestampViewWidth)
dragOffset = max(min(value.translation.width, 0), -maxOffset.width)
}
.onEnded { _ in
dragOffset = .zero
Expand All @@ -79,8 +180,8 @@ public struct MessagesView<MessageT: MessageType, InputBarT: View>: View {
}
}
.listStyle(PlainListStyle())
.messageWidth(context.messageMaxWidth(geometry))
.messageCornerRadius(context.messageCornerRadius)
.messageWidth(context.messageMaxWidth(geometry))
.if(refresh != nil) {
$0.refreshable {
await refresh!.callAsFunction()
Expand Down
110 changes: 3 additions & 107 deletions Sources/AllujaMessages/Views/MessagesViewModifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,104 +35,7 @@ public struct CustomRendererInfo {
}

internal class MessagesViewContext<MessageT: MessageType>: ObservableObject {
init(messages: [MessageT]) {
updateMessages(messages)
}

@Published var messages: [MessageContainer<MessageT>] = []

var maxTimestampViewWidth: CGFloat {
messages.reduce(CGFloat.zero, { res, message in
max(res, message.size.width)
})
}

func updateMessages(_ messages: [MessageT]) {
// Iterate over each message and see if the next one is last
var completeContainers: [MessageContainer<MessageT>] = []
// Whether or not to attempt to place a footer on the next iteration
var footerFallthrough: Bool = false
for (i, message) in messages.sorted(by: { $0.timestamp < $1.timestamp }).enumerated() {
// Figure out what options are needed for each message
var flags: Set<MessageGroupFlag> = []
var timestampFlag: MessageGroupTimestampFlag = .hidden

// If this is the first message OR the last message ends the group then add flag
if completeContainers.isEmpty || completeContainers[completeContainers.index(before: i)].groupFlags.contains(.endGroup) {
if case .collapseTimestamps(let anchor) = groupingOptions.first(where: { item in
if case .collapseTimestamps(_) = item {
return true
}
return false
}), anchor == .top {
timestampFlag = .top
}

flags.insert(.startGroup)
}

if messageEndsGroup(message) {
if case .collapseTimestamps(let anchor) = groupingOptions.first(where: { item in
if case .collapseTimestamps(_) = item {
return true
}
return false
}), anchor == .bottom {
timestampFlag = .bottom
}

flags.insert(.endGroup)
}

// If there aren't any timestamp grouping options, then display timestamp normally
if case .collapseTimestamps(_) = groupingOptions.first(where: { item in
if case .collapseTimestamps(_) = item {
return true
}
return false
}) {

} else {
timestampFlag = .normal
}

switch message.kind {
case .system(_):
if flags.contains(.endGroup) && !flags.contains(.startGroup) && i != messages.startIndex {
completeContainers[completeContainers.index(before: completeContainers.endIndex)].groupFlags.insert(.renderFooter)
} else if flags.contains(.startGroup) && !flags.contains(.endGroup) {
footerFallthrough = true
}
default:
if groupingOptions.contains(.collapseEnclosingViews) {
if flags.contains(.startGroup) {
flags.insert(.renderHeader)
}

if flags.contains(.endGroup) || footerFallthrough {
flags.insert(.renderFooter)
footerFallthrough = false
}
} else {
flags.insert(.renderHeader)
flags.insert(.renderFooter)
}

if groupingOptions.contains(.collapseProfilePicture) {
if flags.contains(.endGroup) {
flags.insert(.renderProfile)
} else {
flags.insert(.renderClearProfile)
}
} else {
flags.insert(.renderProfile)
}
}
completeContainers.append(.init(message: message, groupFlags: flags, timestampFlag: timestampFlag))
}

self.messages = completeContainers
}
init() {}

struct CustomRendererConfiguration: Identifiable {
let id: String
Expand All @@ -156,11 +59,7 @@ internal class MessagesViewContext<MessageT: MessageType>: ObservableObject {
@Published var footer: ((MessageT) -> AnyView)?

/// Configured grouping options
@Published var groupingOptions: [MessageGroupingOption] = [] {
didSet {
updateMessages(messages.map { $0.message })
}
}
@Published var groupingOptions: [MessageGroupingOption] = []

/// `DateFormatter` to use for messages
@Published var messageTimestampFormatter: DateFormatter = DateFormatter()
Expand All @@ -171,10 +70,6 @@ internal class MessagesViewContext<MessageT: MessageType>: ObservableObject {
/// Determines whether or not the current message is the last one in a group, defined in `MessagesView.swift` to allow for access to `messages` array
@Published var messageEndsGroup: (MessageT) -> Bool = { _ in
return true
} {
didSet {
updateMessages(messages.map { $0.message })
}
}

/// Context menu for each message
Expand All @@ -194,6 +89,7 @@ internal class MessagesViewContext<MessageT: MessageType>: ObservableObject {
@Published var messageMaxWidth: ((GeometryProxy) -> CGFloat) = { geometry in
geometry.size.width * 3 / 4
}

@Published var messageCornerRadius: CGFloat = 8.0
}

Expand Down

0 comments on commit c61323c

Please sign in to comment.