Skip to content
Merged
9 changes: 7 additions & 2 deletions .pubnub.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
---
name: swift
scm: github.com/pubnub/swift
version: "9.2.3"
version: "9.3.0"
schema: 1
changelog:
- date: 2025-07-29
version: 9.3.0
changes:
- type: feature
text: "Add the ability to subscribe to presence channels only, without their main counterparts."
- date: 2025-07-29
version: 9.2.3
changes:
Expand Down Expand Up @@ -703,7 +708,7 @@ sdks:
- distribution-type: source
distribution-repository: GitHub release
package-name: PubNub
location: https://github.com/pubnub/swift/archive/refs/tags/9.2.3.zip
location: https://github.com/pubnub/swift/archive/refs/tags/9.3.0.zip
supported-platforms:
supported-operating-systems:
macOS:
Expand Down
16 changes: 8 additions & 8 deletions PubNub.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -4031,7 +4031,7 @@
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 9.2.3;
MARKETING_VERSION = 9.3.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
Expand Down Expand Up @@ -4082,7 +4082,7 @@
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 9.2.3;
MARKETING_VERSION = 9.3.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
Expand Down Expand Up @@ -4190,7 +4190,7 @@
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 9.2.3;
MARKETING_VERSION = 9.3.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
Expand Down Expand Up @@ -4243,7 +4243,7 @@
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 9.2.3;
MARKETING_VERSION = 9.3.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
Expand Down Expand Up @@ -4364,7 +4364,7 @@
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 9.2.3;
MARKETING_VERSION = 9.3.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
Expand Down Expand Up @@ -4416,7 +4416,7 @@
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 9.2.3;
MARKETING_VERSION = 9.3.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17";
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
Expand Down Expand Up @@ -4896,7 +4896,7 @@
"$(TOOLCHAIN_DIR)/usr/lib/swift/macosx",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 9.2.3;
MARKETING_VERSION = 9.3.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14";
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
Expand Down Expand Up @@ -4939,7 +4939,7 @@
"$(TOOLCHAIN_DIR)/usr/lib/swift/macosx",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 9.2.3;
MARKETING_VERSION = 9.3.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14";
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
Expand Down
2 changes: 1 addition & 1 deletion PubNubSwift.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'PubNubSwift'
s.version = '9.2.3'
s.version = '9.3.0'
s.homepage = 'https://github.com/pubnub/swift'
s.documentation_url = 'https://www.pubnub.com/docs/swift-native/pubnub-swift-sdk'
s.authors = { 'PubNub, Inc.' => 'support@pubnub.com' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ class PresenceTransition: TransitionProtocol {

func canTransition(from state: State, dueTo event: Event) -> Bool {
switch event {
case .joined:
return true
case .left:
return true
case let .joined(channels, channelGroups):
return channels.count > 0 || channelGroups.count > 0
case let .left(channels, channelGroups):
return channels.count > 0 || channelGroups.count > 0
case .heartbeatSuccess:
return state is Presence.Heartbeating
case .heartbeatFailed:
Expand Down
204 changes: 64 additions & 140 deletions Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,171 +10,95 @@

import Foundation

/// A container for the current subscribed channels and channel groups
struct SubscribeInput: Equatable {
private let channelEntries: [String: PubNubChannel]
private let groupEntries: [String: PubNubChannel]

// swiftlint:disable:next large_tuple
typealias InsertingResult = (
newInput: SubscribeInput,
insertedChannels: [PubNubChannel],
insertedGroups: [PubNubChannel]
)
// swiftlint:disable:next large_tuple
typealias RemovingResult = (
newInput: SubscribeInput,
removedChannels: [PubNubChannel],
removedGroups: [PubNubChannel]
)

init(channels: [PubNubChannel] = [], groups: [PubNubChannel] = []) {
self.channelEntries = channels.reduce(into: [String: PubNubChannel]()) { r, channel in
_ = r.insert(channel)
}
self.groupEntries = groups.reduce(into: [String: PubNubChannel]()) { r, channel in
_ = r.insert(channel)
}
}

private init(channels: [String: PubNubChannel], groups: [String: PubNubChannel]) {
self.channelEntries = channels
self.groupEntries = groups
}

var isEmpty: Bool {
channelEntries.isEmpty && groupEntries.isEmpty
}

var channels: [PubNubChannel] {
Array(channelEntries.values)
}

var groups: [PubNubChannel] {
Array(groupEntries.values)
}
private let subscribedChannels: Set<String>
private let subscribedChannelGroups: Set<String>

var subscribedChannelNames: [String] {
channelEntries.map { $0.key }
/// Result of comparing two SubscribeInput instances
struct Difference {
/// Items that were added (present in new but not in old)
let addedChannels: Set<String>
/// Channels that were removed (present in old but not in new)
let removedChannels: Set<String>
/// Channel groups that were added (present in new but not in old)
let addedChannelGroups: Set<String>
/// Channel groups that were removed (present in old but not in new)
let removedChannelGroups: Set<String>
}

var subscribedGroupNames: [String] {
groupEntries.map { $0.key }
init(channels: Set<String> = [], channelGroups: Set<String> = []) {
self.subscribedChannels = channels
self.subscribedChannelGroups = channelGroups
}

var allSubscribedChannelNames: [String] {
channelEntries.reduce(into: [String]()) { result, entry in
result.append(entry.value.id)
if entry.value.isPresenceSubscribed {
result.append(entry.value.presenceId)
}
}
init(channels: [String] = [], channelGroups: [String] = []) {
self.subscribedChannels = Set(channels)
self.subscribedChannelGroups = Set(channelGroups)
}

var allSubscribedGroupNames: [String] {
groupEntries.reduce(into: [String]()) { result, entry in
result.append(entry.value.id)
if entry.value.isPresenceSubscribed {
result.append(entry.value.presenceId)
}
}
/// Whether the subscribe input is empty
///
/// This is true if there are no subscribed channels or channel groups
var isEmpty: Bool {
subscribedChannels.isEmpty && subscribedChannelGroups.isEmpty
}

var presenceSubscribedChannelNames: [String] {
channelEntries.compactMap {
if $0.value.isPresenceSubscribed {
return $0.value.id
} else {
return nil
}
}
/// Returns the names of all subscribed channels according to the `withPresence` parameter
///
/// If `withPresence` is true, this list includes both regular and presence channel names
/// If `withPresence` is false, this list does not include presence channel names
func channelNames(withPresence: Bool) -> [String] {
withPresence ? subscribedChannels.allObjects : subscribedChannels.allObjects.filter { !$0.isPresenceChannelName }
}

var presenceSubscribedGroupNames: [String] {
groupEntries.compactMap {
if $0.value.isPresenceSubscribed {
return $0.value.id
} else {
return nil
}
}
/// Returns the names of all subscribed channel groups according to the `withPresence` parameter
///
/// If `withPresence` is true, this list includes both regular and presence channel group names
/// If `withPresence` is false, this list does not include presence channel group names
func channelGroupNames(withPresence: Bool) -> [String] {
withPresence ? subscribedChannelGroups.allObjects : subscribedChannelGroups.allObjects.filter { !$0.isPresenceChannelName }
}

/// Total number of subscribed channels and channel groups
var totalSubscribedCount: Int {
channelEntries.count + groupEntries.count
subscribedChannels.count + subscribedChannelGroups.count
}

func adding(
channels: [PubNubChannel],
and groups: [PubNubChannel]
) -> SubscribeInput.InsertingResult {
// Gets a copy of current channels and channel groups
var currentChannels = channelEntries
var currentGroups = groupEntries

let insertedChannels = channels.filter { currentChannels.insert($0) }
let insertedGroups = groups.filter { currentGroups.insert($0) }

return InsertingResult(
newInput: SubscribeInput(channels: currentChannels, groups: currentGroups),
insertedChannels: insertedChannels,
insertedGroups: insertedGroups
/// Adds the given channels and channel groups and returns a new input without modifying the current one
func adding(channels: Set<String>, and channelGroups: Set<String>) -> SubscribeInput {
SubscribeInput(
channels: channels.union(subscribedChannels),
channelGroups: channelGroups.union(subscribedChannelGroups)
)
}

func removing(
mainChannels: [PubNubChannel],
presenceChannelsOnly: [PubNubChannel],
mainGroups: [PubNubChannel],
presenceGroupsOnly: [PubNubChannel]
) -> SubscribeInput.RemovingResult {
// Gets a copy of current channels and channel groups
var currentChannels = channelEntries
var currentGroups = groupEntries

let removedChannels = mainChannels.compactMap {
currentChannels.removeValue(forKey: $0.id)
} + presenceChannelsOnly.compactMap {
currentChannels.unsubscribePresence($0.id)
}
let removedGroups = mainGroups.compactMap {
currentGroups.removeValue(forKey: $0.id)
} + presenceGroupsOnly.compactMap {
currentGroups.unsubscribePresence($0.id)
}
return RemovingResult(
newInput: SubscribeInput(channels: currentChannels, groups: currentGroups),
removedChannels: removedChannels,
removedGroups: removedGroups
/// Removes the given channels and channel groups and returns a new input without modifying the current one
func removing(channels: Set<String>, and channelGroups: Set<String>) -> SubscribeInput {
SubscribeInput(
channels: subscribedChannels.subtracting(channels),
channelGroups: subscribedChannelGroups.subtracting(channelGroups)
)
}

static func == (lhs: SubscribeInput, rhs: SubscribeInput) -> Bool {
let equalChannels = lhs.allSubscribedChannelNames.sorted(by: <) == rhs.allSubscribedChannelNames.sorted(by: <)
let equalGroups = lhs.allSubscribedGroupNames.sorted(by: <) == rhs.allSubscribedGroupNames.sorted(by: <)
/// Compares this input with another and returns the differences
func difference(from other: SubscribeInput) -> Difference {
let addedChannels = subscribedChannels.subtracting(other.subscribedChannels)
let removedChannels = other.subscribedChannels.subtracting(subscribedChannels)

return equalChannels && equalGroups
}
}
let addedGroups = subscribedChannelGroups.subtracting(other.subscribedChannelGroups)
let removedGroups = other.subscribedChannelGroups.subtracting(subscribedChannelGroups)

extension Dictionary where Key == String, Value == PubNubChannel {
// Inserts and returns the provided channel if that channel doesn't already exist
mutating func insert(_ channel: Value) -> Bool {
if let match = self[channel.id], match == channel {
return false
}
self[channel.id] = channel
return true
return Difference(
addedChannels: addedChannels,
removedChannels: removedChannels,
addedChannelGroups: addedGroups,
removedChannelGroups: removedGroups
)
}

// Updates current Dictionary with the new channel value unsubscribed from Presence.
// Returns the updated value if the corresponding entry matching the passed `id:` was found, otherwise `nil`
@discardableResult mutating func unsubscribePresence(_ id: String) -> Value? {
if let match = self[id], match.isPresenceSubscribed {
let updatedChannel = PubNubChannel(id: match.id, withPresence: false)
self[match.id] = updatedChannel
return updatedChannel
}
return nil
static func == (lhs: SubscribeInput, rhs: SubscribeInput) -> Bool {
lhs.subscribedChannels == rhs.subscribedChannels && lhs.subscribedChannelGroups == rhs.subscribedChannelGroups
}
}

Expand All @@ -183,8 +107,8 @@ extension SubscribeInput: CustomStringConvertible {
String.formattedDescription(
self,
arguments: [
("channels", allSubscribedChannelNames),
("groups", allSubscribedGroupNames)
("channels", channelNames(withPresence: true)),
("groups", channelGroupNames(withPresence: true))
]
)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/PubNub/EventEngine/Subscribe/Subscribe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ extension Subscribe.ReceiveFailedState {
extension Subscribe {
struct UnsubscribedState: SubscribeState {
let cursor: SubscribeCursor = .init(timetoken: 0, region: 0)
let input: SubscribeInput = .init()
let input: SubscribeInput = .init(channels: [], channelGroups: [])
let connectionStatus = ConnectionStatus.disconnected
}
}
Expand Down
Loading
Loading